update some docs and project clean up (only runtime
This commit is contained in:
parent
4f73f46ba9
commit
90ecd77031
769
Cargo.lock
generated
769
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/compiler/languages/prometeu-languages-registry",
|
||||
"crates/compiler/languages/prometeu-language-pbs",
|
||||
|
||||
"crates/compiler/prometeu-build-pipeline",
|
||||
"crates/compiler/prometeu-bytecode",
|
||||
"crates/compiler/prometeu-core",
|
||||
"crates/compiler/prometeu-deps",
|
||||
"crates/compiler/prometeu-language-api",
|
||||
"crates/compiler/prometeu-lowering",
|
||||
|
||||
"crates/console/prometeu-drivers",
|
||||
"crates/console/prometeu-firmware",
|
||||
@ -19,7 +11,6 @@ members = [
|
||||
"crates/host/prometeu-host-desktop-winit",
|
||||
|
||||
"crates/tools/prometeu-cli",
|
||||
"crates/tools/prometeu-lsp",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
[package]
|
||||
name = "prometeu-language-pbs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = ""
|
||||
|
||||
[dependencies]
|
||||
prometeu-language-api = { path = "../../prometeu-language-api" }
|
||||
@ -1,16 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
use prometeu_language_api::{LanguageSpec, SourcePolicy};
|
||||
|
||||
pub static LANGUAGE_SPEC: OnceLock<LanguageSpec> = OnceLock::new();
|
||||
|
||||
fn registry() -> &'static LanguageSpec {
|
||||
LANGUAGE_SPEC.get_or_init(|| {
|
||||
LanguageSpec {
|
||||
id: "pbs",
|
||||
source_policy: SourcePolicy {
|
||||
extensions: vec!["pbs"],
|
||||
case_sensitive: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
mod language_spec;
|
||||
|
||||
pub use language_spec::LANGUAGE_SPEC;
|
||||
@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "prometeu-languages-registry"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = ""
|
||||
|
||||
[dependencies]
|
||||
prometeu-language-api = { path = "../../prometeu-language-api" }
|
||||
|
||||
prometeu-language-pbs = { path = "../prometeu-language-pbs" }
|
||||
@ -1,20 +0,0 @@
|
||||
use prometeu_language_api::LanguageSpec;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use prometeu_language_pbs::LANGUAGE_SPEC as PBS_LANGUAGE_SPEC;
|
||||
|
||||
static REGISTRY: OnceLock<HashMap<&'static str, LanguageSpec>> = OnceLock::new();
|
||||
|
||||
fn registry() -> &'static HashMap<&'static str, LanguageSpec> {
|
||||
let pbs = PBS_LANGUAGE_SPEC.get().unwrap();
|
||||
REGISTRY.get_or_init(|| {
|
||||
HashMap::from([
|
||||
(pbs.id, pbs.clone()),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_language_spec(id: &str) -> Option<&LanguageSpec> {
|
||||
registry().get(id)
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
mod language_spec_registry;
|
||||
|
||||
pub use language_spec_registry::get_language_spec;
|
||||
@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "prometeu-build-pipeline"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "prometeu-build-pipeline"
|
||||
path = "src/main.rs"
|
||||
|
||||
[package.metadata.dist]
|
||||
dist = true
|
||||
include = ["../../VERSION.txt"]
|
||||
|
||||
[dependencies]
|
||||
prometeu-deps = { path = "../prometeu-deps" }
|
||||
prometeu-core = { path = "../prometeu-core" }
|
||||
prometeu-languages-registry = { path = "../languages/prometeu-languages-registry" }
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
anyhow = "1.0.100"
|
||||
camino = "1.2.2"
|
||||
@ -1,158 +0,0 @@
|
||||
use crate::pipeline::run_phases;
|
||||
use crate::{BuildMode, PipelineConfig, PipelineInput, PipelineOutput};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use prometeu_deps::{load_sources, resolve_workspace, DepsConfig};
|
||||
use std::path::{Path, PathBuf};
|
||||
use camino::Utf8Path;
|
||||
use crate::emit_artifacts::{emit_artifacts, EmitOptions};
|
||||
|
||||
/// Command line interface for the Prometeu Compiler.
|
||||
#[derive(Parser)]
|
||||
#[command(name = "prometeu")]
|
||||
#[command(version, about = "PROMETEU toolchain entrypoint", long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
/// Available subcommands for the compiler.
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Builds a Prometeu project by compiling source code into an artifact (pbc/program image).
|
||||
Build {
|
||||
/// Path to the project root directory.
|
||||
project_dir: PathBuf,
|
||||
|
||||
/// Path to save the compiled artifact.
|
||||
/// If omitted, deps/pipeline decide a default under target/ or dist/.
|
||||
#[arg(short, long)]
|
||||
out: Option<PathBuf>,
|
||||
|
||||
/// Whether to generate a .json symbols file for source mapping.
|
||||
#[arg(long, default_value_t = true)]
|
||||
emit_symbols: bool,
|
||||
|
||||
/// Whether to generate a .disasm file for debugging.
|
||||
#[arg(long, default_value_t = true)]
|
||||
emit_disasm: bool,
|
||||
|
||||
/// Whether to explain the dependency resolution process.
|
||||
#[arg(long)]
|
||||
explain_deps: bool,
|
||||
|
||||
/// Build mode (debug/release).
|
||||
#[arg(long, default_value = "debug")]
|
||||
mode: String,
|
||||
},
|
||||
|
||||
/// Verifies if a Prometeu project is valid without emitting code.
|
||||
Verify {
|
||||
project_dir: PathBuf,
|
||||
|
||||
/// Whether to explain the dependency resolution process.
|
||||
#[arg(long)]
|
||||
explain_deps: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Build {
|
||||
project_dir,
|
||||
out,
|
||||
emit_disasm,
|
||||
emit_symbols,
|
||||
explain_deps,
|
||||
mode,
|
||||
} => {
|
||||
let build_mode = parse_mode(&mode)?;
|
||||
let cfg = PipelineConfig {
|
||||
mode: build_mode,
|
||||
enable_cache: true,
|
||||
enable_frontends: false,
|
||||
};
|
||||
|
||||
let pipeline_output = run_pipeline(cfg, &project_dir, explain_deps)
|
||||
.context("pipeline: failed to execute pipeline")?;
|
||||
|
||||
for diagnostics in &pipeline_output.diagnostics {
|
||||
eprintln!("{:?}", diagnostics);
|
||||
}
|
||||
|
||||
let emit_opts = EmitOptions {
|
||||
out,
|
||||
emit_symbols,
|
||||
emit_disasm,
|
||||
};
|
||||
|
||||
emit_artifacts(&emit_opts, &pipeline_output)
|
||||
.context("emit: failed to write artifacts")?;
|
||||
|
||||
if pipeline_output.diagnostics.iter().any(|d| d.severity.is_error()) {
|
||||
anyhow::bail!("build failed due to errors");
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Verify {
|
||||
project_dir,
|
||||
explain_deps,
|
||||
} => {
|
||||
let cfg = PipelineConfig {
|
||||
mode: BuildMode::Test,
|
||||
enable_cache: true,
|
||||
enable_frontends: false,
|
||||
};
|
||||
|
||||
let pipeline_output = run_pipeline(cfg, &project_dir, explain_deps)
|
||||
.context("pipeline: failed to execute pipeline")?;
|
||||
|
||||
for diagnostic in &pipeline_output.diagnostics {
|
||||
eprintln!("{:?}", diagnostic);
|
||||
}
|
||||
|
||||
if pipeline_output.diagnostics.iter().any(|d| d.severity.is_error()) {
|
||||
anyhow::bail!("verify failed due to errors");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn run_pipeline(cfg: PipelineConfig, project_dir: &Path, explain_deps: bool) -> Result<PipelineOutput> {
|
||||
let deps_cfg = DepsConfig {
|
||||
explain: explain_deps,
|
||||
cache_dir: Default::default(),
|
||||
registry_dirs: vec![],
|
||||
};
|
||||
|
||||
let project_dir_path_buf = Utf8Path::from_path(project_dir)
|
||||
.with_context(|| format!("deps: failed to convert project_dir to Utf8Path: {:?}", project_dir))?;
|
||||
let resolved = resolve_workspace(&deps_cfg, project_dir_path_buf)
|
||||
.with_context(|| format!("deps: failed to resolve project at {:?}", project_dir))?;
|
||||
|
||||
let sources = load_sources(&deps_cfg, &resolved)
|
||||
.context("deps: failed to load sources")?;
|
||||
|
||||
let input = PipelineInput {
|
||||
graph: resolved.graph,
|
||||
stack: resolved.stack,
|
||||
sources
|
||||
};
|
||||
|
||||
Ok(run_phases(cfg, input))
|
||||
}
|
||||
|
||||
/// Parse `--mode` from CLI.
|
||||
fn parse_mode(s: &str) -> Result<BuildMode> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"debug" => Ok(BuildMode::Debug),
|
||||
"release" => Ok(BuildMode::Release),
|
||||
"test" => Ok(BuildMode::Test),
|
||||
other => anyhow::bail!("invalid --mode '{}': expected debug|release|test", other),
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BuildMode {
|
||||
Debug,
|
||||
Release,
|
||||
Test,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PipelineConfig {
|
||||
pub mode: BuildMode,
|
||||
pub enable_cache: bool,
|
||||
pub enable_frontends: bool,
|
||||
}
|
||||
|
||||
impl Default for PipelineConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: BuildMode::Debug,
|
||||
enable_cache: true,
|
||||
enable_frontends: false, // Hard Reset default: pipeline runs with no FE.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
use std::any::Any;
|
||||
use prometeu_core::{Diagnostic, FileDB, FileId, NameInterner, ProjectId};
|
||||
use prometeu_deps::BuildStack;
|
||||
|
||||
/// Per-project arena slot created from the BuildStack order.
|
||||
/// The pipeline owns this vector and indexes it by stack position.
|
||||
#[derive(Debug)]
|
||||
pub struct ProjectCtx {
|
||||
pub project_id: ProjectId,
|
||||
|
||||
/// FileIds inserted into `source_db` for this project.
|
||||
pub files: Vec<FileId>,
|
||||
|
||||
/// Frontend output (TypedHIRBundle or similar) - intentionally opaque.
|
||||
pub frontend_out: Option<Box<dyn Any>>,
|
||||
|
||||
/// Backend output (ProgramImage / BytecodeModule / Artifact).
|
||||
/// Keep as opaque until you finalize your bytecode/image crate.
|
||||
pub backend_out: Option<Box<dyn Any>>,
|
||||
}
|
||||
|
||||
impl ProjectCtx {
|
||||
pub fn new(project_id: ProjectId) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
files: Vec::new(),
|
||||
frontend_out: None,
|
||||
backend_out: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PipelineCtx {
|
||||
pub source_db: FileDB,
|
||||
pub interner: NameInterner,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
pub projects: Vec<ProjectCtx>,
|
||||
}
|
||||
|
||||
impl PipelineCtx {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
source_db: FileDB::new(),
|
||||
interner: NameInterner::new(),
|
||||
diagnostics: Vec::new(),
|
||||
projects: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_diagnostic(&mut self, d: Diagnostic) {
|
||||
self.diagnostics.push(d);
|
||||
}
|
||||
|
||||
/// Initialize per-project contexts from the BuildStack order.
|
||||
pub fn init_projects_from_stack(&mut self, stack: &BuildStack) {
|
||||
self.projects.clear();
|
||||
self.projects.reserve(stack.projects.len());
|
||||
for project_id in &stack.projects {
|
||||
self.projects.push(ProjectCtx::new(project_id.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_ctx_mut(&mut self, index_in_stack: usize) -> &mut ProjectCtx {
|
||||
&mut self.projects[index_in_stack]
|
||||
}
|
||||
|
||||
pub fn project_ctx(&self, index_in_stack: usize) -> &ProjectCtx {
|
||||
&self.projects[index_in_stack]
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use crate::PipelineOutput;
|
||||
|
||||
pub struct EmitOptions {
|
||||
pub(crate) out: Option<PathBuf>,
|
||||
pub(crate) emit_symbols: bool,
|
||||
pub(crate) emit_disasm: bool,
|
||||
}
|
||||
|
||||
pub fn emit_artifacts(_opts: &EmitOptions, _outp: &PipelineOutput) -> anyhow::Result<()> {
|
||||
// Later:
|
||||
// - decide output dir (opts.out or default)
|
||||
// - write .pbc / program image
|
||||
// - write symbols.json (if exists)
|
||||
// - write disasm (if exists)
|
||||
Ok(())
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod ctx;
|
||||
pub mod pipeline;
|
||||
pub mod phases;
|
||||
mod emit_artifacts;
|
||||
|
||||
pub use config::*;
|
||||
pub use ctx::*;
|
||||
pub use pipeline::*;
|
||||
pub use cli::run;
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Main entry point for the Prometeu Compiler binary.
|
||||
/// It delegates execution to the library's `run` function.
|
||||
fn main() -> Result<()> {
|
||||
prometeu_build_pipeline::run()
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
use crate::{
|
||||
config::PipelineConfig,
|
||||
ctx::PipelineCtx,
|
||||
pipeline::{PipelineInput},
|
||||
};
|
||||
|
||||
pub fn run(_cfg: &PipelineConfig, input: &PipelineInput, ctx: &mut PipelineCtx) {
|
||||
// Arena init: one ProjectCtx per project in stack order.
|
||||
ctx.init_projects_from_stack(&input.stack);
|
||||
|
||||
// NOTE: no filesystem, no FE/BE assumptions here.
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
use crate::{config::PipelineConfig, ctx::PipelineCtx, pipeline::{Artifacts, PipelineInput}};
|
||||
|
||||
pub fn run(_cfg: &PipelineConfig, _input: &PipelineInput, _ctx: &mut PipelineCtx) -> Artifacts {
|
||||
// Hard Reset stub:
|
||||
// - later: emit build outputs (to FS via deps if you want strict IO centralization).
|
||||
Artifacts::default()
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
use crate::{config::PipelineConfig, ctx::PipelineCtx, pipeline::PipelineInput};
|
||||
|
||||
pub fn run(cfg: &PipelineConfig, _input: &PipelineInput, _ctx: &mut PipelineCtx) {
|
||||
if !cfg.enable_frontends {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hard Reset:
|
||||
// - no FE wired yet.
|
||||
// - later: iterate projects in stack order and call FE plugin(s).
|
||||
}
|
||||
@ -1,117 +0,0 @@
|
||||
use prometeu_core::{Diagnostic, Severity, Span};
|
||||
use prometeu_deps::LoadedSources;
|
||||
use crate::{
|
||||
config::PipelineConfig,
|
||||
ctx::PipelineCtx,
|
||||
pipeline::PipelineInput,
|
||||
};
|
||||
|
||||
pub fn run(_cfg: &PipelineConfig, input: &PipelineInput, ctx: &mut PipelineCtx) {
|
||||
load_sources(&input.sources, ctx);
|
||||
|
||||
for i in 0..ctx.projects.len() {
|
||||
let is_empty = ctx.projects[i].files.is_empty();
|
||||
|
||||
if is_empty {
|
||||
let project_id = &input.stack.projects[i];
|
||||
let project_name = input.graph.project(project_id).unwrap().name.clone();
|
||||
|
||||
ctx.push_diagnostic(Diagnostic {
|
||||
severity: Severity::Warning,
|
||||
code: "PIPELINE_NO_SOURCES".into(),
|
||||
message: format!(
|
||||
"Project '{}' has no source files loaded.",
|
||||
project_name
|
||||
),
|
||||
span: Span::none(),
|
||||
related: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_sources(sources: &LoadedSources, ctx: &mut PipelineCtx) {
|
||||
let stack_len = ctx.projects.len();
|
||||
let src_len = sources.per_project.len();
|
||||
|
||||
// 1) Diagnostic is sizes don't match
|
||||
if src_len != stack_len {
|
||||
ctx.push_diagnostic(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
code: "PIPELINE_SOURCES_STACK_LEN_MISMATCH".into(),
|
||||
message: format!(
|
||||
"LoadedSources.per_project len ({}) does not match BuildStack len ({}).",
|
||||
src_len, stack_len
|
||||
),
|
||||
span: Span::none(),
|
||||
related: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Process the bare minimum (don't panic, just keep running with diagnostics)
|
||||
let n = stack_len.min(src_len);
|
||||
|
||||
for i in 0..n {
|
||||
let expected = ctx.projects[i].project_id;
|
||||
let got = sources.per_project[i].project_id;
|
||||
|
||||
if got != expected {
|
||||
ctx.push_diagnostic(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
code: "PIPELINE_SOURCES_STACK_ORDER_MISMATCH".into(),
|
||||
message: format!(
|
||||
"LoadedSources is not aligned with BuildStack at index {}: expected project_id {:?}, got {:?}.",
|
||||
i, expected, got
|
||||
),
|
||||
span: Span::none(),
|
||||
related: vec![],
|
||||
});
|
||||
|
||||
// there is no fix tolerance here, if it is wrong, it is wrong
|
||||
// just catch as much diagnostics as possible before "crashing"
|
||||
continue;
|
||||
}
|
||||
|
||||
for f in &sources.per_project[i].files {
|
||||
let file_id = ctx.source_db.upsert(&f.uri, &f.text);
|
||||
ctx.projects[i].files.push(file_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) If any LoadSources remains, it is a deps bug
|
||||
if src_len > stack_len {
|
||||
for extra in &sources.per_project[stack_len..] {
|
||||
ctx.push_diagnostic(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
code: "PIPELINE_SOURCES_EXTRA_PROJECT".into(),
|
||||
message: format!(
|
||||
"LoadedSources contains extra project_id {:?} not present in BuildStack.",
|
||||
extra.project_id
|
||||
),
|
||||
span: Span::none(),
|
||||
related: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4) If missing inputs, it is another deps bug...
|
||||
if stack_len > src_len {
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
for missing in &ctx.projects[src_len..] {
|
||||
diagnostics.push(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
code: "PIPELINE_SOURCES_MISSING_PROJECT".into(),
|
||||
message: format!(
|
||||
"LoadedSources missing sources for project_id {:?} present in BuildStack.",
|
||||
missing.project_id
|
||||
),
|
||||
span: Span::none(),
|
||||
related: vec![],
|
||||
});
|
||||
}
|
||||
for diagnostic in diagnostics {
|
||||
ctx.push_diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
use crate::{config::PipelineConfig, ctx::PipelineCtx, pipeline::PipelineInput};
|
||||
|
||||
pub fn run(_cfg: &PipelineConfig, _input: &PipelineInput, _ctx: &mut PipelineCtx) {
|
||||
// Hard Reset stub:
|
||||
// - later: consume TypedHIRBundle(s) and lower into ProgramImage/BytecodeModule.
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
pub mod boot;
|
||||
pub mod load_source;
|
||||
pub mod language;
|
||||
pub mod lowering;
|
||||
pub mod emit;
|
||||
@ -1,59 +0,0 @@
|
||||
use crate::{config::PipelineConfig, ctx::PipelineCtx, phases};
|
||||
use prometeu_core::Diagnostic;
|
||||
use prometeu_deps::{BuildStack, LoadedSources, ResolvedGraph};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PipelineInput {
|
||||
pub graph: ResolvedGraph,
|
||||
pub stack: BuildStack,
|
||||
pub sources: LoadedSources
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PipelineStats {
|
||||
pub projects_count: usize,
|
||||
pub files_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Artifacts {
|
||||
// placeholder: later include produced ProgramImage(s), debug bundles, logs, etc.
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PipelineOutput {
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
pub artifacts: Artifacts,
|
||||
pub stats: PipelineStats,
|
||||
}
|
||||
|
||||
pub(crate) fn run_phases(cfg: PipelineConfig, input: PipelineInput) -> PipelineOutput {
|
||||
let mut ctx = PipelineCtx::new();
|
||||
|
||||
// Boot: create project slots in arena order.
|
||||
phases::boot::run(&cfg, &input, &mut ctx);
|
||||
|
||||
// Load source: populate FileDB from LoadedSources.
|
||||
phases::load_source::run(&cfg, &input, &mut ctx);
|
||||
|
||||
// Frontend phase (stub / optional).
|
||||
phases::language::run(&cfg, &input, &mut ctx);
|
||||
|
||||
// Backend phase (stub).
|
||||
phases::lowering::run(&cfg, &input, &mut ctx);
|
||||
|
||||
// Emit phase (stub).
|
||||
let artifacts = phases::emit::run(&cfg, &input, &mut ctx);
|
||||
|
||||
// Stats (basic).
|
||||
let mut stats = PipelineStats::default();
|
||||
stats.projects_count = ctx.projects.len();
|
||||
stats.files_count = ctx.projects.iter().map(|p| p.files.len()).sum();
|
||||
|
||||
PipelineOutput {
|
||||
diagnostics: ctx.diagnostics,
|
||||
artifacts,
|
||||
stats,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,3 @@
|
||||
//! # Prometeu Bytecode (PBC)
|
||||
//!
|
||||
//! This crate defines the core Application Binary Interface (ABI) and Instruction Set Architecture (ISA)
|
||||
//! for the Prometeu Virtual Machine (PVM).
|
||||
//!
|
||||
//! It serves as the "source of truth" for how programs are structured, encoded, and executed
|
||||
//! within the ecosystem.
|
||||
//!
|
||||
//! ## Core Components:
|
||||
//! - [`opcode`]: Defines the available instructions and their performance characteristics.
|
||||
//! - [`abi`]: Specifies the binary rules for operands and stack behavior.
|
||||
//! - [`asm`]: Provides a programmatic Assembler to convert high-level instructions to bytes.
|
||||
//! - [`disasm`]: Provides a Disassembler to inspect compiled bytecode.
|
||||
//! - [`readwrite`]: Internal utilities for Little-Endian binary I/O.
|
||||
|
||||
pub mod opcode;
|
||||
pub mod opcode_spec;
|
||||
pub mod abi;
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
[package]
|
||||
name = "prometeu-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
prometeu-bytecode = { path = "../prometeu-bytecode" }
|
||||
@ -1,3 +0,0 @@
|
||||
mod source;
|
||||
|
||||
pub use source::*;
|
||||
@ -1,81 +0,0 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use crate::Span;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Severity {
|
||||
Error,
|
||||
Warning,
|
||||
}
|
||||
|
||||
impl Severity {
|
||||
pub fn is_error(&self) -> bool {
|
||||
match self {
|
||||
Severity::Error => true,
|
||||
Severity::Warning => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Severity {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Severity::Error => serializer.serialize_str("error"),
|
||||
Severity::Warning => serializer.serialize_str("warning"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Diagnostic {
|
||||
pub severity: Severity,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub span: Span,
|
||||
pub related: Vec<(String, Span)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DiagnosticBundle {
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl DiagnosticBundle {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
diagnostics: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, diagnostic: Diagnostic) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
pub fn error(code: &str, message: String, span: Span) -> Self {
|
||||
let mut bundle = Self::new();
|
||||
bundle.push(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
code: code.to_string(),
|
||||
message,
|
||||
span,
|
||||
related: Vec::new(),
|
||||
});
|
||||
bundle
|
||||
}
|
||||
|
||||
pub fn has_errors(&self) -> bool {
|
||||
self.diagnostics
|
||||
.iter()
|
||||
.any(|d| matches!(d.severity, Severity::Error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Diagnostic> for DiagnosticBundle {
|
||||
fn from(diagnostic: Diagnostic) -> Self {
|
||||
let mut bundle = Self::new();
|
||||
bundle.push(diagnostic);
|
||||
bundle
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::FileId;
|
||||
use crate::LineIndex;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct FileDB {
|
||||
files: Vec<FileData>,
|
||||
uri_to_id: HashMap<String, FileId>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FileData {
|
||||
uri: String,
|
||||
text: String,
|
||||
line_index: LineIndex,
|
||||
}
|
||||
|
||||
impl FileDB {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
files: Vec::new(),
|
||||
uri_to_id: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upsert(&mut self, uri: &str, text: &str) -> FileId {
|
||||
if let Some(&id) = self.uri_to_id.get(uri) {
|
||||
let line_index = LineIndex::new(&text);
|
||||
self.files[id.0 as usize] = FileData {
|
||||
uri: uri.to_owned(),
|
||||
text: text.to_owned(),
|
||||
line_index,
|
||||
};
|
||||
id
|
||||
} else {
|
||||
let id = FileId(self.files.len() as u32);
|
||||
let line_index = LineIndex::new(&text);
|
||||
self.files.push(FileData {
|
||||
uri: uri.to_owned(),
|
||||
text: text.to_owned(),
|
||||
line_index,
|
||||
});
|
||||
self.uri_to_id.insert(uri.to_string(), id);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_id(&self, uri: &str) -> Option<FileId> {
|
||||
self.uri_to_id.get(uri).copied()
|
||||
}
|
||||
|
||||
pub fn uri(&self, id: FileId) -> &str {
|
||||
&self.files[id.0 as usize].uri
|
||||
}
|
||||
|
||||
pub fn text(&self, id: FileId) -> &str {
|
||||
&self.files[id.0 as usize].text
|
||||
}
|
||||
|
||||
pub fn line_index(&self, id: FileId) -> &LineIndex {
|
||||
&self.files[id.0 as usize].line_index
|
||||
}
|
||||
|
||||
/// Returns a list of all known file IDs in insertion order.
|
||||
pub fn all_files(&self) -> Vec<FileId> {
|
||||
(0..self.files.len()).map(|i| FileId(i as u32)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
macro_rules! define_id {
|
||||
($name:ident) => {
|
||||
#[repr(transparent)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct $name(pub u32);
|
||||
|
||||
impl $name {
|
||||
pub const NONE: $name = $name(u32::MAX);
|
||||
|
||||
#[inline]
|
||||
pub const fn as_u32(self) -> u32 { self.0 }
|
||||
|
||||
#[inline]
|
||||
pub fn is_none(self) -> bool {
|
||||
self == $name::NONE
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for $name {
|
||||
#[inline]
|
||||
fn from(value: u32) -> Self { Self(value) }
|
||||
}
|
||||
|
||||
impl From<$name> for u32 {
|
||||
#[inline]
|
||||
fn from(value: $name) -> Self { value.0 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_id!(FileId);
|
||||
define_id!(NodeId);
|
||||
define_id!(NameId);
|
||||
define_id!(SymbolId);
|
||||
define_id!(TypeId);
|
||||
define_id!(ModuleId);
|
||||
define_id!(ProjectId);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use std::mem::size_of;
|
||||
|
||||
#[test]
|
||||
fn ids_are_repr_transparent_and_hashable() {
|
||||
assert_eq!(size_of::<FileId>(), 4);
|
||||
assert_eq!(size_of::<NodeId>(), 4);
|
||||
assert_eq!(size_of::<NameId>(), 4);
|
||||
assert_eq!(size_of::<SymbolId>(), 4);
|
||||
assert_eq!(size_of::<TypeId>(), 4);
|
||||
assert_eq!(size_of::<ModuleId>(), 4);
|
||||
assert_eq!(size_of::<ProjectId>(), 4);
|
||||
|
||||
// Hash/Eq usage
|
||||
let mut m: HashMap<SymbolId, &str> = HashMap::new();
|
||||
m.insert(SymbolId(1), "one");
|
||||
assert_eq!(m.get(&SymbolId(1)).copied(), Some("one"));
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
#[derive(Debug)]
|
||||
pub struct LineIndex {
|
||||
line_starts: Vec<u32>,
|
||||
total_len: u32,
|
||||
}
|
||||
|
||||
impl LineIndex {
|
||||
pub fn new(text: &str) -> Self {
|
||||
let mut line_starts = vec![0];
|
||||
for (offset, c) in text.char_indices() {
|
||||
if c == '\n' {
|
||||
line_starts.push((offset + 1) as u32);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
line_starts,
|
||||
total_len: text.len() as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn offset_to_line_col(&self, offset: u32) -> (u32, u32) {
|
||||
let line = match self.line_starts.binary_search(&offset) {
|
||||
Ok(line) => line as u32,
|
||||
Err(line) => (line - 1) as u32,
|
||||
};
|
||||
let col = offset - self.line_starts[line as usize];
|
||||
(line, col)
|
||||
}
|
||||
|
||||
pub fn line_col_to_offset(&self, line: u32, col: u32) -> Option<u32> {
|
||||
let start = *self.line_starts.get(line as usize)?;
|
||||
let offset = start + col;
|
||||
|
||||
let next_start = self.line_starts.get(line as usize + 1).copied().unwrap_or(self.total_len);
|
||||
if offset < next_start || (offset == next_start && offset == self.total_len) {
|
||||
Some(offset)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
mod ids;
|
||||
mod span;
|
||||
mod file_db;
|
||||
mod name_interner;
|
||||
mod diagnostics;
|
||||
mod line_index;
|
||||
|
||||
pub use ids::*;
|
||||
pub use span::Span;
|
||||
pub use file_db::FileDB;
|
||||
pub use line_index::LineIndex;
|
||||
pub use name_interner::NameInterner;
|
||||
pub use diagnostics::*;
|
||||
@ -1,56 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::NameId;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NameInterner {
|
||||
names: Vec<String>,
|
||||
ids: HashMap<String, NameId>,
|
||||
}
|
||||
|
||||
impl NameInterner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
names: Vec::new(),
|
||||
ids: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intern(&mut self, s: &str) -> NameId {
|
||||
if let Some(id) = self.ids.get(s) {
|
||||
return *id;
|
||||
}
|
||||
|
||||
let id = NameId(self.names.len() as u32);
|
||||
self.names.push(s.to_string());
|
||||
self.ids.insert(self.names[id.0 as usize].clone(), id);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn get(&self, s: &str) -> Option<NameId> {
|
||||
self.ids.get(s).copied()
|
||||
}
|
||||
|
||||
pub fn resolve(&self, id: NameId) -> &str {
|
||||
&self.names[id.0 as usize]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn interner_intern_resolve_roundtrip() {
|
||||
let mut interner = NameInterner::new();
|
||||
let id = interner.intern("foo");
|
||||
assert_eq!(interner.resolve(id), "foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interner_dedups_strings() {
|
||||
let mut interner = NameInterner::new();
|
||||
let id1 = interner.intern("bar");
|
||||
let id2 = interner.intern("bar");
|
||||
assert_eq!(id1, id2);
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
use crate::FileId;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Span {
|
||||
pub file: FileId,
|
||||
pub start: u32, // byte offset
|
||||
pub end: u32, // byte offset, exclusive
|
||||
}
|
||||
|
||||
impl Span {
|
||||
#[inline]
|
||||
pub fn new(file: FileId, start: u32, end: u32) -> Self {
|
||||
Self { file, start, end }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
file: FileId::NONE,
|
||||
start: 0,
|
||||
end: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_none(&self) -> bool {
|
||||
self.file.is_none()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn len(&self) -> u32 {
|
||||
self.end.saturating_sub(self.start)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn contains(&self, byte: u32) -> bool {
|
||||
self.start <= byte && byte < self.end
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
use prometeu_core::{FileDB, LineIndex};
|
||||
|
||||
#[test]
|
||||
fn test_line_index_roundtrip() {
|
||||
let text = "line 1\nline 2\nline 3";
|
||||
let index = LineIndex::new(text);
|
||||
|
||||
// Roundtrip for each character
|
||||
for (offset, _) in text.char_indices() {
|
||||
let (line, col) = index.offset_to_line_col(offset as u32);
|
||||
let recovered_offset = index.line_col_to_offset(line, col).expect("Should recover offset");
|
||||
assert_eq!(offset as u32, recovered_offset, "Offset mismatch at line {}, col {}", line, col);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line_index_boundaries() {
|
||||
let text = "a\nbc\n";
|
||||
let index = LineIndex::new(text);
|
||||
|
||||
// "a" -> (0, 0)
|
||||
assert_eq!(index.offset_to_line_col(0), (0, 0));
|
||||
assert_eq!(index.line_col_to_offset(0, 0), Some(0));
|
||||
|
||||
// "\n" -> (0, 1)
|
||||
assert_eq!(index.offset_to_line_col(1), (0, 1));
|
||||
assert_eq!(index.line_col_to_offset(0, 1), Some(1));
|
||||
|
||||
// "b" -> (1, 0)
|
||||
assert_eq!(index.offset_to_line_col(2), (1, 0));
|
||||
assert_eq!(index.line_col_to_offset(1, 0), Some(2));
|
||||
|
||||
// "c" -> (1, 1)
|
||||
assert_eq!(index.offset_to_line_col(3), (1, 1));
|
||||
assert_eq!(index.line_col_to_offset(1, 1), Some(3));
|
||||
|
||||
// "\n" (second) -> (1, 2)
|
||||
assert_eq!(index.offset_to_line_col(4), (1, 2));
|
||||
assert_eq!(index.line_col_to_offset(1, 2), Some(4));
|
||||
|
||||
// EOF (after last \n) -> (2, 0)
|
||||
assert_eq!(index.offset_to_line_col(5), (2, 0));
|
||||
assert_eq!(index.line_col_to_offset(2, 0), Some(5));
|
||||
|
||||
// Out of bounds
|
||||
assert_eq!(index.line_col_to_offset(2, 1), None);
|
||||
assert_eq!(index.line_col_to_offset(3, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_db_upsert_and_access() {
|
||||
let mut db = FileDB::new();
|
||||
let uri = "file:///test.txt";
|
||||
let text = "hello\nworld".to_string();
|
||||
|
||||
let id = db.upsert(uri, text.clone());
|
||||
assert_eq!(db.file_id(uri), Some(id));
|
||||
assert_eq!(db.uri(id), uri);
|
||||
assert_eq!(db.text(id), &text);
|
||||
|
||||
let index = db.line_index(id);
|
||||
assert_eq!(index.offset_to_line_col(6), (1, 0)); // 'w' is at offset 6
|
||||
|
||||
// Update existing file
|
||||
let new_text = "new content".to_string();
|
||||
let same_id = db.upsert(uri, new_text.clone());
|
||||
assert_eq!(id, same_id);
|
||||
assert_eq!(db.text(id), &new_text);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
use prometeu_core::{FileId, Span};
|
||||
|
||||
#[test]
|
||||
fn span_end_is_exclusive() {
|
||||
let file = FileId(1);
|
||||
let s = Span::new(file, 2, 5);
|
||||
// len = end - start
|
||||
assert_eq!(s.len(), 3);
|
||||
// contains is [start, end)
|
||||
assert!(s.contains(2));
|
||||
assert!(s.contains(3));
|
||||
assert!(s.contains(4));
|
||||
assert!(!s.contains(5));
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "prometeu-deps"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = ""
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
prometeu-core = { path = "../prometeu-core" }
|
||||
prometeu-language-api = { path = "../prometeu-language-api" }
|
||||
prometeu-languages-registry = { path = "../languages/prometeu-languages-registry" }
|
||||
anyhow = "1.0.101"
|
||||
camino = "1.2.2"
|
||||
walkdir = "2.5.0"
|
||||
serde_json = "1.0.149"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@ -1,19 +0,0 @@
|
||||
mod model;
|
||||
mod load_sources;
|
||||
mod workspace;
|
||||
|
||||
pub use workspace::resolve_workspace;
|
||||
pub use load_sources::load_sources;
|
||||
|
||||
pub use model::manifest::*;
|
||||
pub use model::resolved_project::ResolvedWorkspace;
|
||||
pub use model::deps_config::DepsConfig;
|
||||
pub use model::project_descriptor::ProjectDescriptor;
|
||||
pub use model::build_stack::BuildStack;
|
||||
pub use model::resolved_graph::ResolvedGraph;
|
||||
pub use model::loaded_sources::LoadedSources;
|
||||
pub use model::project_sources::ProjectSources;
|
||||
pub use model::loaded_file::LoadedFile;
|
||||
pub use model::cache_blobs::CacheBlobs;
|
||||
pub use model::cache_plan::CachePlan;
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use camino::Utf8PathBuf;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::{
|
||||
DepsConfig,
|
||||
LoadedFile,
|
||||
LoadedSources,
|
||||
ProjectSources,
|
||||
ResolvedWorkspace,
|
||||
};
|
||||
|
||||
pub fn load_sources(cfg: &DepsConfig, resolved: &ResolvedWorkspace) -> Result<LoadedSources> {
|
||||
let mut per_project = Vec::with_capacity(resolved.stack.projects.len());
|
||||
|
||||
for project_id in &resolved.stack.projects {
|
||||
let project = resolved
|
||||
.graph
|
||||
.project(project_id)
|
||||
.with_context(|| format!("deps: unknown project_id {:?} in build stack", project_id))?;
|
||||
|
||||
if cfg.explain {
|
||||
eprintln!(
|
||||
"[deps] load_sources: project {}@{} ({:?})",
|
||||
project.name, project.version, project.project_dir
|
||||
);
|
||||
}
|
||||
|
||||
let mut files: Vec<LoadedFile> = Vec::new();
|
||||
|
||||
for root in &project.source_roots {
|
||||
let abs_root = project.project_dir.join(root);
|
||||
|
||||
if cfg.explain {
|
||||
eprintln!("[deps] scanning {:?}", abs_root);
|
||||
}
|
||||
|
||||
if !abs_root.exists() {
|
||||
anyhow::bail!(
|
||||
"deps: source root does not exist for project {}@{}: {:?}",
|
||||
project.name,
|
||||
project.version,
|
||||
abs_root
|
||||
);
|
||||
}
|
||||
|
||||
// Walk recursively.
|
||||
for entry in WalkDir::new(&abs_root)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let ft = entry.file_type();
|
||||
if !ft.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
|
||||
// TODO: precisamos mexer no prometeu.json para configurar o frontend do projeto
|
||||
// Filter extensions: start with PBS only.
|
||||
if path.extension().and_then(|s| s.to_str()) != Some("pbs") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert to Utf8Path (the best effort) and use a stable "uri".
|
||||
let path_utf8: Utf8PathBuf = match Utf8PathBuf::from_path_buf(path.to_path_buf()) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
anyhow::bail!("deps: non-utf8 path found while scanning sources: {:?}", path);
|
||||
}
|
||||
};
|
||||
|
||||
let text = std::fs::read_to_string(&path_utf8)
|
||||
.with_context(|| format!("deps: failed to read source file {:?}", path_utf8))?;
|
||||
|
||||
// TODO: normalize newlines
|
||||
|
||||
files.push(LoadedFile {
|
||||
uri: path_utf8.to_string(),
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Determinism: sort a file list by uri (important for stable builds).
|
||||
files.sort_by(|a, b| a.uri.cmp(&b.uri));
|
||||
|
||||
per_project.push(ProjectSources {
|
||||
project_id: project_id.clone(),
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(LoadedSources { per_project })
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
use prometeu_core::ProjectId;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildStack {
|
||||
pub projects: Vec<ProjectId>,
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
/// Cache blobs computed/validated by deps.
|
||||
/// The pipeline may decide when to store, but deps executes IO and cache validity.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheBlobs {
|
||||
// placeholder
|
||||
pub _unused: (),
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachePlan {
|
||||
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
use camino::Utf8PathBuf;
|
||||
|
||||
pub struct DepsConfig {
|
||||
pub explain: bool,
|
||||
pub cache_dir: Utf8PathBuf,
|
||||
pub registry_dirs: Vec<Utf8PathBuf>, // or sources ?
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadedFile {
|
||||
pub uri: String,
|
||||
pub text: String,
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
use crate::model::project_sources::ProjectSources;
|
||||
|
||||
/// Sources already loaded by deps (IO happens in deps, not in pipeline).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadedSources {
|
||||
/// For each project in the stack, a list of files (uri + text).
|
||||
pub per_project: Vec<ProjectSources>,
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
use camino::Utf8PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Manifest {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub source_roots: Vec<String>,
|
||||
|
||||
pub language: LanguageDecl,
|
||||
|
||||
#[serde(default)]
|
||||
pub deps: Vec<DepDecl>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LanguageDecl {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum DepDecl {
|
||||
Local {
|
||||
path: String,
|
||||
},
|
||||
Git {
|
||||
git: String,
|
||||
rev: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PrometeuLock {
|
||||
pub schema: u32,
|
||||
#[serde(default)]
|
||||
pub mappings: Vec<LockMapping>,
|
||||
}
|
||||
|
||||
impl PrometeuLock {
|
||||
pub fn blank() -> Self {
|
||||
Self {
|
||||
schema: 0,
|
||||
mappings: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup_git_local_dir(&self, url: &str, rev: &str) -> Option<&String> {
|
||||
self.mappings.iter().find_map(|m| match m {
|
||||
LockMapping::Git {
|
||||
git, rev: r, local_dir
|
||||
} if git == url && r == rev => Some(local_dir),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
pub enum LockMapping {
|
||||
Git {
|
||||
git: String,
|
||||
rev: String,
|
||||
local_dir: String,
|
||||
},
|
||||
Registry {
|
||||
registry: String,
|
||||
version: String,
|
||||
local_dir: String,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
pub mod deps_config;
|
||||
pub mod project_descriptor;
|
||||
pub mod build_stack;
|
||||
pub mod resolved_graph;
|
||||
pub mod loaded_sources;
|
||||
pub mod project_sources;
|
||||
pub mod loaded_file;
|
||||
pub mod cache_blobs;
|
||||
pub mod resolved_project;
|
||||
pub mod cache_plan;
|
||||
pub mod manifest;
|
||||
@ -1,14 +0,0 @@
|
||||
use camino::Utf8PathBuf;
|
||||
use prometeu_core::ProjectId;
|
||||
use prometeu_language_api::SourcePolicy;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjectDescriptor {
|
||||
pub project_id: ProjectId,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub project_dir: Utf8PathBuf,
|
||||
pub source_roots: Vec<Utf8PathBuf>,
|
||||
pub language_id: String,
|
||||
pub source_policy: SourcePolicy,
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
use prometeu_core::ProjectId;
|
||||
use crate::model::loaded_file::LoadedFile;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjectSources {
|
||||
pub project_id: ProjectId,
|
||||
pub files: Vec<LoadedFile>,
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
use prometeu_core::ProjectId;
|
||||
use crate::ProjectDescriptor;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedGraph {
|
||||
pub root: ProjectId,
|
||||
pub projects: Vec<ProjectDescriptor>, // arena
|
||||
// opcional: adjacency list para checks
|
||||
pub edges: Vec<Vec<ProjectId>>, // edges[from] = vec[to]
|
||||
}
|
||||
|
||||
impl ResolvedGraph {
|
||||
pub fn project(&self, id: &ProjectId) -> Option<&ProjectDescriptor> {
|
||||
self.projects.get(id.0 as usize)
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
use prometeu_core::ProjectId;
|
||||
use crate::{BuildStack, ResolvedGraph};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedWorkspace {
|
||||
pub project_id: ProjectId,
|
||||
pub graph: ResolvedGraph,
|
||||
pub stack: BuildStack,
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
|
||||
use crate::workspace::model::DepRef;
|
||||
|
||||
pub trait DepsHost {
|
||||
fn read_to_string(&self, path: &Utf8Path) -> Result<String>;
|
||||
|
||||
// fn ensure_project_local(&self, from_dir: &Utf8Path, dep: &DepRef) -> Result<Utf8PathBuf>;
|
||||
}
|
||||
|
||||
pub struct FsHost;
|
||||
|
||||
impl DepsHost for FsHost {
|
||||
fn read_to_string(&self, path: &Utf8Path) -> Result<String> {
|
||||
std::fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read {:?}", path))
|
||||
}
|
||||
|
||||
// fn ensure_project_local(&self, from_dir: &Utf8Path, dep: &DepRef) -> Result<Utf8PathBuf> {
|
||||
// match dep {
|
||||
// DepRef::Local { path } => {
|
||||
// let joined = from_dir.join(path);
|
||||
// let canon = joined.canonicalize()
|
||||
// .with_context(|| format!("deps: dep path does not exist: {:?}", joined))?;
|
||||
// Utf8PathBuf::from_path_buf(canon)
|
||||
// .map_err(|p| anyhow::anyhow!("deps: non-utf8 dep dir: {:?}", p))
|
||||
// }
|
||||
// _ => unimplemented!(),
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
mod resolve_workspace;
|
||||
mod host;
|
||||
mod model;
|
||||
mod phases;
|
||||
|
||||
pub use resolve_workspace::resolve_workspace;
|
||||
@ -1,31 +0,0 @@
|
||||
use camino::Utf8PathBuf;
|
||||
use prometeu_core::ProjectId;
|
||||
use prometeu_language_api::SourcePolicy;
|
||||
|
||||
use crate::Manifest;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawProjectNode {
|
||||
pub dir: Utf8PathBuf,
|
||||
pub manifest_path: Utf8PathBuf,
|
||||
pub manifest: Manifest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DepRef {
|
||||
Local {
|
||||
path: Utf8PathBuf
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjectNode {
|
||||
pub id: ProjectId,
|
||||
pub dir: Utf8PathBuf,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub source_roots: Vec<Utf8PathBuf>,
|
||||
pub language_id: String,
|
||||
pub deps: Vec<DepRef>,
|
||||
pub source_policy: SourcePolicy,
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
use crate::model::manifest::DepDecl;
|
||||
use crate::workspace::host::DepsHost;
|
||||
use crate::workspace::model::RawProjectNode;
|
||||
use crate::workspace::phases::state::ResolverState;
|
||||
use crate::Manifest;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use camino::Utf8PathBuf;
|
||||
use serde_json;
|
||||
use std::fs::canonicalize;
|
||||
|
||||
/// Phase 1: Discover all projects in the workspace.
|
||||
///
|
||||
/// - Reads `prometeu.json` from each pending project directory.
|
||||
/// - Parses `Manifest`.
|
||||
/// - Registers the raw node.
|
||||
/// - Enqueues local-path deps for discovery (v0).
|
||||
///
|
||||
/// Does NOT:
|
||||
/// - assign ProjectId
|
||||
/// - build edges
|
||||
/// - validate versions
|
||||
pub fn discover(
|
||||
cfg: &crate::DepsConfig,
|
||||
host: &dyn DepsHost,
|
||||
state: &mut ResolverState,
|
||||
) -> Result<()> {
|
||||
while let Some(canon_dir) = state.pending.pop_front() {
|
||||
// de-dup by directory
|
||||
if state.raw_by_dir.contains_key(&canon_dir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let manifest_path = canon_dir.join("prometeu.json");
|
||||
if !manifest_path.exists() || !manifest_path.is_file() {
|
||||
bail!(
|
||||
"deps: manifest not found: expected a file {:?} (project dir {:?})",
|
||||
manifest_path,
|
||||
canon_dir
|
||||
);
|
||||
}
|
||||
|
||||
if cfg.explain {
|
||||
eprintln!("[deps][discover] reading {:?}", manifest_path);
|
||||
}
|
||||
|
||||
let text = host
|
||||
.read_to_string(&manifest_path)
|
||||
.with_context(|| format!("deps: failed to read manifest {:?}", manifest_path))?;
|
||||
|
||||
let manifest: Manifest = serde_json::from_str(&text)
|
||||
.with_context(|| format!("deps: invalid manifest JSON {:?}", manifest_path))?;
|
||||
|
||||
// Register raw node
|
||||
let raw_idx = state.raw.len();
|
||||
state.raw.push(RawProjectNode {
|
||||
dir: canon_dir.clone(),
|
||||
manifest_path: manifest_path.clone(),
|
||||
manifest: manifest.clone(),
|
||||
});
|
||||
state.raw_by_dir.insert(canon_dir.clone(), raw_idx);
|
||||
|
||||
for dep in &manifest.deps {
|
||||
match dep {
|
||||
DepDecl::Local { path } => {
|
||||
let dep_dir = canon_dir.join(path);
|
||||
|
||||
let dep_dir_std = dep_dir.canonicalize().with_context(|| {
|
||||
format!(
|
||||
"deps: dep path does not exist: {:?} (from {:?})",
|
||||
dep_dir, canon_dir
|
||||
)
|
||||
})?;
|
||||
|
||||
let dep_dir_canon = Utf8PathBuf::from_path_buf(dep_dir_std)
|
||||
.map_err(|p| anyhow!("deps: non-utf8 dep dir: {:?}", p))?;
|
||||
|
||||
if cfg.explain {
|
||||
eprintln!("[deps][discover] local dep '{}' -> {:?}", path, dep_dir_canon);
|
||||
}
|
||||
state.pending.push_back(dep_dir_canon);
|
||||
}
|
||||
|
||||
DepDecl::Git { git, rev } => {
|
||||
let Some(rev) = rev.as_deref() else {
|
||||
bail!(
|
||||
"deps: git dependency '{}' requires an explicit 'rev' (commit hash) for now",
|
||||
git
|
||||
);
|
||||
};
|
||||
|
||||
let Some(local_dir) = state.lock.lookup_git_local_dir(git, rev) else {
|
||||
bail!(
|
||||
"deps: git dependency requires prometeu.lock mapping, but entry not found: git='{}' rev='{}'",
|
||||
git,
|
||||
rev
|
||||
);
|
||||
};
|
||||
|
||||
// canonicalize the lock-provided local dir to keep identity stable
|
||||
let local_dir_std = canonicalize(local_dir)
|
||||
.with_context(|| format!("deps: prometeu.lock local_dir does not exist: {:?}", local_dir))?;
|
||||
|
||||
let local_dir_canon = Utf8PathBuf::from_path_buf(local_dir_std)
|
||||
.map_err(|p| anyhow!("deps: non-utf8 lock local_dir: {:?}", p))?;
|
||||
|
||||
// validate manifest exists at the mapped project root
|
||||
// (this check should not belong here, but it is ok)
|
||||
let mapped_manifest = local_dir_canon.join("prometeu.json");
|
||||
if !mapped_manifest.exists() || !mapped_manifest.is_file() {
|
||||
bail!(
|
||||
"deps: prometeu.lock maps git dep to {:?}, but manifest is missing: {:?}",
|
||||
local_dir_canon,
|
||||
mapped_manifest
|
||||
);
|
||||
}
|
||||
|
||||
if cfg.explain {
|
||||
eprintln!(
|
||||
"[deps][discover] git dep '{}' rev '{}' -> {:?}",
|
||||
git, rev, local_dir_canon
|
||||
);
|
||||
}
|
||||
|
||||
state.pending.push_back(local_dir_canon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use prometeu_core::ProjectId;
|
||||
|
||||
use crate::workspace::model::DepRef;
|
||||
use crate::workspace::phases::state::ResolverState;
|
||||
|
||||
/// Phase 3: Localize dependencies and build graph edges.
|
||||
///
|
||||
/// For each project node:
|
||||
/// - For each DepRef:
|
||||
/// - host.ensure_project_local(from_dir, dep) -> dep_dir (local on disk)
|
||||
/// - map dep_dir to ProjectId via st.by_dir
|
||||
/// - st.edges[from].push(dep_id)
|
||||
///
|
||||
/// v0 policy:
|
||||
/// - Only DepRef::LocalPath is supported.
|
||||
/// - Git/Registry cause a hard error (future extension point).
|
||||
pub fn localize(cfg: &crate::DepsConfig, state: &mut ResolverState) -> Result<()> {
|
||||
// Reset edges (allows re-run / deterministic behavior)
|
||||
for e in &mut state.edges {
|
||||
e.clear();
|
||||
}
|
||||
|
||||
for from_idx in 0..state.nodes.len() {
|
||||
let from_id: ProjectId = state.nodes[from_idx].id;
|
||||
let from_dir = state.nodes[from_idx].dir.clone();
|
||||
|
||||
if cfg.explain {
|
||||
eprintln!(
|
||||
"[deps][localize] from id={:?} dir={:?}",
|
||||
from_id, from_dir
|
||||
);
|
||||
}
|
||||
|
||||
// Clone deps to avoid borrow conflicts (simple + safe for now)
|
||||
let deps = state.nodes[from_idx].deps.clone();
|
||||
|
||||
for dep in deps {
|
||||
match &dep {
|
||||
DepRef::Local {
|
||||
path
|
||||
} => {
|
||||
let dep_id = state.by_dir.get(path).copied().with_context(|| {
|
||||
format!(
|
||||
"deps: localized dep dir {:?} was not discovered; \
|
||||
ensure the dep has a prometeu.json and is reachable via local paths",
|
||||
path
|
||||
)
|
||||
})?;
|
||||
state.edges[from_id.0 as usize].push(dep_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: keep edges deterministic
|
||||
state.edges[from_id.0 as usize].sort_by_key(|id| id.0);
|
||||
state.edges[from_id.0 as usize].dedup();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,144 +0,0 @@
|
||||
use crate::model::manifest::DepDecl;
|
||||
use crate::workspace::model::{DepRef, ProjectNode};
|
||||
use crate::workspace::phases::state::ResolverState;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use camino::Utf8PathBuf;
|
||||
use prometeu_core::ProjectId;
|
||||
use prometeu_languages_registry::get_language_spec;
|
||||
use std::fs::canonicalize;
|
||||
|
||||
/// Phase 2: Materialize projects (allocate ProjectId / arena nodes).
|
||||
///
|
||||
/// Inputs:
|
||||
/// - st.raw (RawProjectNode: dir + manifest)
|
||||
///
|
||||
/// Outputs:
|
||||
/// - st.nodes (ProjectNode arena)
|
||||
/// - st.by_dir (dir -> ProjectId)
|
||||
/// - st.edges (allocated adjacency lists, empty for now)
|
||||
/// - st.root (ProjectId for root_dir)
|
||||
///
|
||||
/// Does NOT:
|
||||
/// - resolve deps to local dirs (that's phase localize)
|
||||
/// - validate version conflicts/cycles
|
||||
/// - resolve language/source policy
|
||||
pub fn materialize(cfg: &crate::DepsConfig, state: &mut ResolverState) -> Result<()> {
|
||||
// Reset materialized state (allows rerun in future refactors/tests)
|
||||
state.nodes.clear();
|
||||
state.by_dir.clear();
|
||||
state.edges.clear();
|
||||
state.root = None;
|
||||
|
||||
state.nodes.reserve(state.raw.len());
|
||||
state.edges.reserve(state.raw.len());
|
||||
|
||||
for (idx, raw) in state.raw.iter().enumerate() {
|
||||
let id = ProjectId(idx as u32);
|
||||
|
||||
// Default source roots if omitted
|
||||
let source_roots: Vec<Utf8PathBuf> = raw
|
||||
.manifest
|
||||
.source_roots
|
||||
.iter()
|
||||
.map(|root| Utf8PathBuf::from(root))
|
||||
.collect();
|
||||
if source_roots.is_empty() {
|
||||
bail!(
|
||||
"deps: no source roots specified for project {}",
|
||||
raw.manifest.name
|
||||
)
|
||||
}
|
||||
|
||||
// Convert DepDecl -> DepRef (no localization yet)
|
||||
let mut deps: Vec<DepRef> = Vec::with_capacity(raw.manifest.deps.len());
|
||||
for d in &raw.manifest.deps {
|
||||
match d {
|
||||
DepDecl::Local { path } => {
|
||||
let joined = raw.dir.join(path);
|
||||
let dir_std = joined.canonicalize()
|
||||
.with_context(|| format!("deps: local dep path does not exist: {:?} (from {:?})", joined, raw.dir))?;
|
||||
|
||||
let dir_canon = Utf8PathBuf::from_path_buf(dir_std)
|
||||
.map_err(|p| anyhow!("deps: non-utf8 dep dir: {:?}", p))?;
|
||||
deps.push(DepRef::Local {
|
||||
path: dir_canon
|
||||
});
|
||||
}
|
||||
DepDecl::Git { git, rev } => {
|
||||
let Some(rev) = rev.as_deref() else {
|
||||
bail!(
|
||||
"deps: git dependency '{}' requires an explicit 'rev' (commit hash) for now",
|
||||
git
|
||||
);
|
||||
};
|
||||
|
||||
let Some(local_dir) = state.lock.lookup_git_local_dir(git, rev) else {
|
||||
bail!(
|
||||
"deps: git dependency requires prometeu.lock mapping, but entry not found: git='{}' rev='{}'",
|
||||
git,
|
||||
rev
|
||||
);
|
||||
};
|
||||
|
||||
// canonicalize the lock-provided local dir to keep identity stable
|
||||
let path = canonicalize(local_dir).with_context(|| {
|
||||
format!(
|
||||
"deps: prometeu.lock local_dir does not exist: {:?}",
|
||||
local_dir
|
||||
)
|
||||
})?;
|
||||
|
||||
let local_dir_canon = Utf8PathBuf::from_path_buf(path)
|
||||
.map_err(|p| anyhow!("deps: non-utf8 lock local_dir: {:?}", p))?;
|
||||
|
||||
deps.push(DepRef::Local {
|
||||
path: local_dir_canon,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.explain {
|
||||
eprintln!(
|
||||
"[deps][materialize] id={:?} {}@{} dir={:?} language={}",
|
||||
id, raw.manifest.name, raw.manifest.version, raw.dir, raw.manifest.language.id
|
||||
);
|
||||
}
|
||||
|
||||
let source_policy = get_language_spec(raw.manifest.language.id.as_str())
|
||||
.map(|spec| spec.source_policy.clone())
|
||||
.ok_or(anyhow!(
|
||||
"deps: unknown language spec: {}",
|
||||
raw.manifest.language.id
|
||||
))?;
|
||||
|
||||
// Record node
|
||||
state.nodes.push(ProjectNode {
|
||||
id,
|
||||
dir: raw.dir.clone(),
|
||||
name: raw.manifest.name.clone(),
|
||||
version: raw.manifest.version.clone(),
|
||||
source_roots,
|
||||
language_id: raw.manifest.language.id.clone(),
|
||||
deps,
|
||||
source_policy,
|
||||
});
|
||||
|
||||
state.by_dir.insert(raw.dir.clone(), id);
|
||||
state.edges.push(Vec::new());
|
||||
}
|
||||
|
||||
// Determine root id
|
||||
if let Some(root_id) = state.by_dir.get(&state.root_dir).copied() {
|
||||
state.root = Some(root_id);
|
||||
} else {
|
||||
// This should never happen if seed/discover worked.
|
||||
// Keep it as a hard failure (in a later validate phase you can convert to a nicer diagnostic).
|
||||
anyhow::bail!(
|
||||
"deps: root project dir {:?} was not discovered/materialized",
|
||||
state.root_dir
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
mod run_all;
|
||||
mod state;
|
||||
mod discover;
|
||||
mod materialize;
|
||||
mod localize;
|
||||
mod validate;
|
||||
mod policy;
|
||||
mod stack;
|
||||
|
||||
pub use run_all::run_all;
|
||||
@ -1,17 +0,0 @@
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::workspace::phases::state::ResolverState;
|
||||
|
||||
pub fn policy(_cfg: &crate::DepsConfig, state: &mut ResolverState) -> Result<()> {
|
||||
for node in &state.nodes {
|
||||
if node.source_policy.extensions.is_empty() {
|
||||
bail!(
|
||||
"deps: project {}@{} has empty source_policy.extensions (language={})",
|
||||
node.name,
|
||||
node.version,
|
||||
node.language_id
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use camino::Utf8Path;
|
||||
|
||||
use crate::{BuildStack, DepsConfig, ProjectDescriptor, ResolvedGraph, ResolvedWorkspace};
|
||||
use crate::workspace::host::FsHost;
|
||||
use crate::workspace::phases::{discover, localize, materialize, policy, stack, state, validate};
|
||||
|
||||
pub fn run_all(cfg: &DepsConfig, fs_host: &FsHost, root_dir: &Utf8Path) -> Result<ResolvedWorkspace> {
|
||||
let mut st = state::seed(cfg, root_dir)?;
|
||||
|
||||
discover::discover(cfg, fs_host, &mut st)?;
|
||||
materialize::materialize(cfg, &mut st)?;
|
||||
localize::localize(cfg, &mut st)?;
|
||||
|
||||
validate::validate(cfg, &st)?;
|
||||
policy::policy(cfg, &mut st)?;
|
||||
|
||||
let build_stack: BuildStack = stack::stack(cfg, &mut st)?;
|
||||
|
||||
let root = st
|
||||
.root
|
||||
.context("deps: internal error: root ProjectId not set")?;
|
||||
|
||||
// Build the arena expected by ResolvedGraph: index == ProjectId.0
|
||||
// materialize already assigns ProjectId(idx), so st.nodes order is stable.
|
||||
let mut projects: Vec<ProjectDescriptor> = Vec::with_capacity(st.nodes.len());
|
||||
for n in &st.nodes {
|
||||
projects.push(ProjectDescriptor {
|
||||
project_id: n.id,
|
||||
name: n.name.clone(),
|
||||
version: n.version.clone(),
|
||||
project_dir: n.dir.clone(),
|
||||
source_roots: n.source_roots.clone(),
|
||||
language_id: n.language_id.clone(),
|
||||
source_policy: n.source_policy.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let graph = ResolvedGraph {
|
||||
root,
|
||||
projects,
|
||||
edges: st.edges,
|
||||
};
|
||||
|
||||
Ok(ResolvedWorkspace {
|
||||
project_id: root,
|
||||
graph,
|
||||
stack: build_stack,
|
||||
})
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use prometeu_core::ProjectId;
|
||||
use std::collections::VecDeque;
|
||||
use crate::BuildStack;
|
||||
use crate::workspace::phases::state::ResolverState;
|
||||
|
||||
/// Phase: BuildStack (deps-first topo order).
|
||||
///
|
||||
/// Output:
|
||||
/// - state.stack: Vec<ProjectId> where deps appear before dependents.
|
||||
///
|
||||
/// Determinism:
|
||||
/// - ties are resolved by ProjectId order (stable across runs if discovery is stable).
|
||||
pub fn stack(cfg: &crate::DepsConfig, state: &mut ResolverState) -> Result<BuildStack> {
|
||||
let n = state.nodes.len();
|
||||
let _root = state.root.context("deps: internal error: root ProjectId not set")?;
|
||||
|
||||
// Build indegree
|
||||
let mut indeg = vec![0usize; n];
|
||||
for outs in &state.edges {
|
||||
for &to in outs {
|
||||
indeg[to.0 as usize] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic queue: push in ProjectId order
|
||||
let mut q = VecDeque::new();
|
||||
for i in 0..n {
|
||||
if indeg[i] == 0 {
|
||||
q.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
let mut order: Vec<ProjectId> = Vec::with_capacity(n);
|
||||
|
||||
while let Some(i) = q.pop_front() {
|
||||
order.push(ProjectId(i as u32));
|
||||
|
||||
// Ensure deterministic traversal of outgoing edges too
|
||||
// (your localize already sort/dedup edges, but this doesn't hurt)
|
||||
for &to in &state.edges[i] {
|
||||
let j = to.0 as usize;
|
||||
indeg[j] -= 1;
|
||||
if indeg[j] == 0 {
|
||||
// Deterministic insert: keep queue ordered by ProjectId
|
||||
// Simple O(n) insertion is fine for now.
|
||||
insert_sorted_by_id(&mut q, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If validate ran, this should already be cycle-free; still keep a guard.
|
||||
if order.len() != n {
|
||||
anyhow::bail!(
|
||||
"deps: internal error: stack generation did not visit all nodes ({} of {})",
|
||||
order.len(),
|
||||
n
|
||||
);
|
||||
}
|
||||
|
||||
if cfg.explain {
|
||||
eprintln!("[deps][stack] build order:");
|
||||
for id in &order {
|
||||
let node = &state.nodes[id.0 as usize];
|
||||
eprintln!(" - {:?} {}@{} dir={:?}", id, node.name, node.version, node.dir);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BuildStack {
|
||||
projects: order,
|
||||
})
|
||||
}
|
||||
|
||||
/// Insert node index `i` into queue `q` keeping it sorted by ProjectId (index).
|
||||
fn insert_sorted_by_id(q: &mut VecDeque<usize>, i: usize) {
|
||||
// Common fast path: append if >= last
|
||||
if let Some(&last) = q.back() {
|
||||
if i >= last {
|
||||
q.push_back(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise find insertion point
|
||||
let mut pos = 0usize;
|
||||
for &v in q.iter() {
|
||||
if i < v {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
// VecDeque has no insert, so rebuild (small sizes OK for hard reset)
|
||||
let mut tmp: Vec<usize> = q.drain(..).collect();
|
||||
tmp.insert(pos, i);
|
||||
*q = VecDeque::from(tmp);
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use anyhow::Context;
|
||||
use crate::workspace::model::{RawProjectNode, ProjectNode};
|
||||
use prometeu_core::ProjectId;
|
||||
use crate::PrometeuLock;
|
||||
use serde_json;
|
||||
|
||||
pub struct ResolverState {
|
||||
pub root_dir: Utf8PathBuf,
|
||||
|
||||
// phase1 output
|
||||
pub raw: Vec<RawProjectNode>,
|
||||
pub raw_by_dir: HashMap<Utf8PathBuf, usize>,
|
||||
pub pending: VecDeque<Utf8PathBuf>,
|
||||
|
||||
// phase2+
|
||||
pub nodes: Vec<ProjectNode>,
|
||||
pub by_dir: HashMap<Utf8PathBuf, ProjectId>,
|
||||
pub edges: Vec<Vec<ProjectId>>,
|
||||
|
||||
pub root: Option<ProjectId>,
|
||||
|
||||
pub lock: PrometeuLock,
|
||||
}
|
||||
|
||||
pub fn seed(_cfg: &crate::DepsConfig, root_dir: &Utf8Path) -> anyhow::Result<ResolverState> {
|
||||
let path_buf = root_dir.canonicalize()?;
|
||||
let root_dir_canon = Utf8PathBuf::from_path_buf(path_buf)
|
||||
.map_err(|p| anyhow::anyhow!("deps: non-utf8 root dir: {:?}", p))?;
|
||||
|
||||
let lock_path = root_dir_canon.join("prometeu.lock");
|
||||
let lock = if lock_path.exists() {
|
||||
let txt = std::fs::read_to_string(&lock_path)?;
|
||||
serde_json::from_str::<PrometeuLock>(&txt)
|
||||
.with_context(|| format!("invalid prometeu.lock at {:?}", lock_path))?
|
||||
} else {
|
||||
PrometeuLock::blank()
|
||||
};
|
||||
|
||||
let mut pending = VecDeque::new();
|
||||
pending.push_back(root_dir_canon.clone());
|
||||
|
||||
Ok(ResolverState {
|
||||
root_dir: root_dir_canon.clone(),
|
||||
raw: vec![],
|
||||
raw_by_dir: HashMap::new(),
|
||||
pending,
|
||||
|
||||
nodes: vec![],
|
||||
by_dir: HashMap::new(),
|
||||
edges: vec![],
|
||||
|
||||
root: None,
|
||||
|
||||
lock,
|
||||
})
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use prometeu_core::ProjectId;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use crate::workspace::phases::state::ResolverState;
|
||||
|
||||
/// Phase: Validate workspace graph & invariants (v0).
|
||||
///
|
||||
/// Checks:
|
||||
/// - root present
|
||||
/// - edges are in-range
|
||||
/// - no cycles
|
||||
/// - no version conflicts for same project name
|
||||
pub fn validate(cfg: &crate::DepsConfig, state: &ResolverState) -> Result<()> {
|
||||
// 1) root present
|
||||
let root = state.root.context("deps: internal error: root ProjectId not set")?;
|
||||
if cfg.explain {
|
||||
eprintln!("[deps][validate] root={:?}", root);
|
||||
}
|
||||
|
||||
// 2) edges sanity
|
||||
let n = state.nodes.len();
|
||||
for (from_idx, outs) in state.edges.iter().enumerate() {
|
||||
for &to in outs {
|
||||
let to_idx = to.0 as usize;
|
||||
if to_idx >= n {
|
||||
bail!(
|
||||
"deps: invalid edge: from {:?} -> {:?} (to out of range; nodes={})",
|
||||
ProjectId(from_idx as u32),
|
||||
to,
|
||||
n
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) version conflicts by name
|
||||
// name -> (version -> ProjectId)
|
||||
let mut by_name: HashMap<&str, HashMap<&str, ProjectId>> = HashMap::new();
|
||||
for node in &state.nodes {
|
||||
let vmap = by_name.entry(node.name.as_str()).or_default();
|
||||
vmap.entry(node.version.as_str()).or_insert(node.id);
|
||||
}
|
||||
for (name, versions) in &by_name {
|
||||
if versions.len() > 1 {
|
||||
// create deterministic message
|
||||
let mut vs: Vec<(&str, ProjectId)> = versions.iter().map(|(v, id)| (*v, *id)).collect();
|
||||
vs.sort_by(|a, b| a.0.cmp(b.0));
|
||||
|
||||
let mut msg = format!("deps: version conflict for project '{}':", name);
|
||||
for (v, id) in vs {
|
||||
let dir = &state.nodes[id.0 as usize].dir;
|
||||
msg.push_str(&format!("\n - {} at {:?} (id={:?})", v, dir, id));
|
||||
}
|
||||
bail!(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) cycle detection (Kahn + leftover nodes)
|
||||
// Build indegree
|
||||
let mut indeg = vec![0usize; n];
|
||||
for outs in &state.edges {
|
||||
for &to in outs {
|
||||
indeg[to.0 as usize] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut q = VecDeque::new();
|
||||
for i in 0..n {
|
||||
if indeg[i] == 0 {
|
||||
q.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
let mut visited = 0usize;
|
||||
while let Some(i) = q.pop_front() {
|
||||
visited += 1;
|
||||
for &to in &state.edges[i] {
|
||||
let j = to.0 as usize;
|
||||
indeg[j] -= 1;
|
||||
if indeg[j] == 0 {
|
||||
q.push_back(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if visited != n {
|
||||
// Nodes with indeg>0 are part of cycles (or downstream of them)
|
||||
let mut cyclic: Vec<ProjectId> = Vec::new();
|
||||
for i in 0..n {
|
||||
if indeg[i] > 0 {
|
||||
cyclic.push(ProjectId(i as u32));
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic error output
|
||||
cyclic.sort_by_key(|id| id.0);
|
||||
|
||||
let mut msg = "deps: dependency cycle detected among:".to_string();
|
||||
for id in cyclic {
|
||||
let node = &state.nodes[id.0 as usize];
|
||||
msg.push_str(&format!("\n - {:?} {}@{} dir={:?}", id, node.name, node.version, node.dir));
|
||||
}
|
||||
bail!(msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use camino::Utf8Path;
|
||||
|
||||
use crate::{DepsConfig, ResolvedWorkspace};
|
||||
use crate::workspace::host::FsHost;
|
||||
|
||||
pub fn resolve_workspace(cfg: &DepsConfig, root_dir: &Utf8Path) -> Result<ResolvedWorkspace> {
|
||||
let host = FsHost;
|
||||
crate::workspace::phases::run_all(cfg, &host, root_dir)
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
[package]
|
||||
name = "prometeu-language-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Canonical language contract for Prometeu Backend: identifiers, references, and strict Frontend trait."
|
||||
repository = "https://github.com/prometeu/runtime"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SourcePolicy {
|
||||
pub extensions: Vec<&'static str>,
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
impl SourcePolicy {
|
||||
pub fn matches_ext(&self, ext: &str) -> bool {
|
||||
if self.case_sensitive {
|
||||
self.extensions.iter().any(|e| *e == ext)
|
||||
} else {
|
||||
self.extensions.iter().any(|e| e.eq_ignore_ascii_case(ext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LanguageSpec {
|
||||
pub id: &'static str,
|
||||
pub source_policy: SourcePolicy,
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
mod language_spec;
|
||||
|
||||
pub use language_spec::*;
|
||||
@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "prometeu-lowering"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
prometeu-bytecode = { path = "../prometeu-bytecode" }
|
||||
prometeu-core = { path = "../prometeu-core" }
|
||||
prometeu-language-api = { path = "../prometeu-language-api" }
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
anyhow = "1.0.100"
|
||||
pathdiff = "0.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.1"
|
||||
@ -6,6 +6,4 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.149"
|
||||
prometeu-vm = { path = "../prometeu-vm" }
|
||||
prometeu-core = { path = "../../compiler/prometeu-core" }
|
||||
prometeu-hal = { path = "../prometeu-hal" }
|
||||
|
||||
@ -5,11 +5,6 @@ edition = "2024"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
prometeu-drivers = { path = "../prometeu-drivers" }
|
||||
prometeu-vm = { path = "../prometeu-vm" }
|
||||
prometeu-system = { path = "../prometeu-system" }
|
||||
prometeu-bytecode = { path = "../../compiler/prometeu-bytecode" }
|
||||
prometeu-core = { path = "../../compiler/prometeu-core" }
|
||||
prometeu-hal = { path = "../prometeu-hal" }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
|
||||
@ -5,7 +5,6 @@ edition = "2024"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
prometeu-core = { path = "../../compiler/prometeu-core" }
|
||||
prometeu-bytecode = { path = "../../compiler/prometeu-bytecode" }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
@ -8,7 +8,6 @@ license.workspace = true
|
||||
serde_json = "1.0.149"
|
||||
prometeu-vm = { path = "../prometeu-vm" }
|
||||
prometeu-bytecode = { path = "../../compiler/prometeu-bytecode" }
|
||||
prometeu-core = { path = "../../compiler/prometeu-core" }
|
||||
prometeu-hal = { path = "../prometeu-hal" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@ -5,7 +5,5 @@ edition = "2024"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
prometeu-bytecode = { path = "../../compiler/prometeu-bytecode" }
|
||||
prometeu-core = { path = "../../compiler/prometeu-core" }
|
||||
prometeu-hal = { path = "../prometeu-hal" }
|
||||
|
||||
@ -114,6 +114,12 @@ pub struct GateEntry {
|
||||
pub type_id: u32,
|
||||
}
|
||||
|
||||
impl Default for VirtualMachine {
|
||||
fn default() -> Self {
|
||||
Self::new(vec![], vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualMachine {
|
||||
/// Creates a new VM instance with the provided bytecode and constants.
|
||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
|
||||
@ -242,15 +248,6 @@ impl VirtualMachine {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Default for VirtualMachine {
|
||||
fn default() -> Self {
|
||||
Self::new(vec![], vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualMachine {
|
||||
/// Executes the VM for a limited number of cycles (budget).
|
||||
///
|
||||
/// This is the heart of the deterministic execution model. Instead of running
|
||||
@ -359,10 +356,6 @@ impl VirtualMachine {
|
||||
/// 2. Decode: Identify what operation to perform.
|
||||
/// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals.
|
||||
pub fn step(&mut self, native: &mut dyn NativeInterface, ctx: &mut HostContext) -> Result<(), LogicalFrameEndingReason> {
|
||||
self.step_impl(native, ctx)
|
||||
}
|
||||
|
||||
fn step_impl(&mut self, native: &mut dyn NativeInterface, ctx: &mut HostContext) -> Result<(), LogicalFrameEndingReason> {
|
||||
if self.halted || self.pc >= self.program.rom.len() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ dist = true
|
||||
include = ["../../VERSION.txt"]
|
||||
|
||||
[dependencies]
|
||||
prometeu-core = { path = "../../compiler/prometeu-core" }
|
||||
prometeu-firmware = { path = "../../console/prometeu-firmware" }
|
||||
prometeu-system = { path = "../../console/prometeu-system" }
|
||||
prometeu-drivers = { path = "../../console/prometeu-drivers" }
|
||||
|
||||
@ -13,10 +13,6 @@ path = "src/main.rs"
|
||||
name = "prometeu-runtime"
|
||||
path = "src/bin/prometeu-runtime.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "prometeuc"
|
||||
path = "src/bin/prometeuc.rs"
|
||||
|
||||
[package.metadata.dist]
|
||||
dist = true
|
||||
include = [
|
||||
@ -27,5 +23,4 @@ include = [
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
prometeu-host-desktop-winit = { path = "../../host/prometeu-host-desktop-winit" }
|
||||
prometeu-build-pipeline = { path = "../../compiler/prometeu-build-pipeline" }
|
||||
anyhow = "1.0.100"
|
||||
|
||||
@ -1 +0,0 @@
|
||||
fn main() -> anyhow::Result<()> { prometeu_build_pipeline::run() }
|
||||
@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "prometeu-lsp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
tower-lsp = "0.20"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7" }
|
||||
prometeu-core = { path = "../../compiler/prometeu-core" }
|
||||
prometeu-build-pipeline = { path = "../../compiler/prometeu-build-pipeline" }
|
||||
@ -1,51 +0,0 @@
|
||||
// use std::sync::Arc;
|
||||
// use tokio::sync::RwLock;
|
||||
// use tokio_util::sync::CancellationToken;
|
||||
//
|
||||
// use std::collections::HashMap;
|
||||
// use prometeu_core::{FileDB, FileId, ProjectId};
|
||||
// use crate::rebuild::LspDiagnostic;
|
||||
// use crate::rebuild::FlatSymbol;
|
||||
//
|
||||
// #[derive(Default)]
|
||||
// pub struct AnalysisDb {
|
||||
// pub file_db: FileDB,
|
||||
// pub file_to_project: HashMap<FileId, ProjectId>,
|
||||
//
|
||||
// // Os campos abaixo serão conectados conforme PR-03/04/05 (podem começar como None)
|
||||
// // pub ast: Option<AstArena>,
|
||||
// // pub symbols: Option<SymbolArena>,
|
||||
// // pub types: Option<TypeArena>,
|
||||
// // pub diagnostics: Vec<Diagnostic>,
|
||||
//
|
||||
// /// Incrementa a cada rebuild concluído com sucesso
|
||||
// pub revision: u64,
|
||||
//
|
||||
// /// Cancel token do último rebuild em progresso (se houver)
|
||||
// pub active_rebuild: Option<CancellationToken>,
|
||||
//
|
||||
// /// Último snapshot bom (consultado pelos handlers LSP)
|
||||
// pub last_good: Option<AnalysisSnapshot>,
|
||||
// }
|
||||
//
|
||||
// pub type SharedDb = Arc<RwLock<AnalysisDb>>;
|
||||
//
|
||||
// impl AnalysisDb {
|
||||
// pub fn project_for_file(&self, file: FileId) -> Option<ProjectId> {
|
||||
// self.file_to_project.get(&file).copied()
|
||||
// }
|
||||
//
|
||||
// /// Returns all known file ids in the FileDB.
|
||||
// pub fn file_ids(&self) -> Vec<FileId> {
|
||||
// // delegate to FileDB helper (added in prometeu-analysis)
|
||||
// self.file_db.all_files()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[derive(Default, Clone)]
|
||||
// pub struct AnalysisSnapshot {
|
||||
// /// Diagnostics por arquivo (URI LSP → diagnostics já convertidos)
|
||||
// pub diagnostics_by_uri: HashMap<String, Vec<LspDiagnostic>>,
|
||||
// /// Lista “flatten” de símbolos para workspaceSymbol/documentSymbol
|
||||
// pub symbols_flat: Vec<FlatSymbol>,
|
||||
// }
|
||||
@ -1,190 +0,0 @@
|
||||
// use std::sync::Arc;
|
||||
// use tokio::sync::RwLock;
|
||||
// use tower_lsp::{Client, LspService, Server};
|
||||
// use tower_lsp::lsp_types as lsp;
|
||||
//
|
||||
// mod analysis_db;
|
||||
// mod rebuild;
|
||||
|
||||
// struct Backend {
|
||||
// db: SharedDb,
|
||||
// client: Client,
|
||||
// }
|
||||
|
||||
// #[tower_lsp::async_trait]
|
||||
// impl tower_lsp::LanguageServer for Backend {
|
||||
// async fn initialize(
|
||||
// &self,
|
||||
// _: tower_lsp::lsp_types::InitializeParams,
|
||||
// ) -> tower_lsp::jsonrpc::Result<tower_lsp::lsp_types::InitializeResult> {
|
||||
// Ok(tower_lsp::lsp_types::InitializeResult {
|
||||
// capabilities: tower_lsp::lsp_types::ServerCapabilities {
|
||||
// text_document_sync: Some(
|
||||
// tower_lsp::lsp_types::TextDocumentSyncCapability::Kind(
|
||||
// tower_lsp::lsp_types::TextDocumentSyncKind::FULL,
|
||||
// ),
|
||||
// ),
|
||||
// // MVP capabilities only (PR-08):
|
||||
// definition_provider: Some(tower_lsp::lsp_types::OneOf::Left(true)),
|
||||
// document_symbol_provider: Some(tower_lsp::lsp_types::OneOf::Left(true)),
|
||||
// // workspace_symbol is not available in tower-lsp 0.20 trait
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ..Default::default()
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// async fn initialized(&self, _: tower_lsp::lsp_types::InitializedParams) {}
|
||||
//
|
||||
// async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// // didOpen: upsert texto, solicita rebuild
|
||||
// async fn did_open(&self, params: tower_lsp::lsp_types::DidOpenTextDocumentParams) {
|
||||
// let uri = params.text_document.uri.to_string();
|
||||
// let text = params.text_document.text;
|
||||
// {
|
||||
// let mut guard = self.db.write().await;
|
||||
// guard.file_db.upsert(&uri, text);
|
||||
// }
|
||||
// rebuild::request_rebuild(self.db.clone(), self.client.clone()).await;
|
||||
// }
|
||||
//
|
||||
// // didChange (FULL): receber conteúdo completo e upsert
|
||||
// async fn did_change(&self, params: tower_lsp::lsp_types::DidChangeTextDocumentParams) {
|
||||
// let uri = params.text_document.uri.to_string();
|
||||
// // Full-sync: esperamos 1 conteúdo completo
|
||||
// if let Some(change) = params.content_changes.into_iter().last() {
|
||||
// let text = change.text;
|
||||
// let mut guard = self.db.write().await;
|
||||
// guard.file_db.upsert(&uri, text);
|
||||
// }
|
||||
// rebuild::request_rebuild(self.db.clone(), self.client.clone()).await;
|
||||
// }
|
||||
//
|
||||
// // didClose: opcionalmente remover do db e limpar diagnostics
|
||||
// async fn did_close(&self, params: tower_lsp::lsp_types::DidCloseTextDocumentParams) {
|
||||
// let uri = params.text_document.uri;
|
||||
// // Estratégia simples: manter FileDB para estabilidade de IDs, mas limpar diagnostics
|
||||
// let _ = self
|
||||
// .client
|
||||
// .publish_diagnostics(uri.clone(), vec![], Some(0))
|
||||
// .await;
|
||||
// }
|
||||
//
|
||||
// async fn goto_definition(
|
||||
// &self,
|
||||
// params: tower_lsp::lsp_types::GotoDefinitionParams,
|
||||
// ) -> tower_lsp::jsonrpc::Result<Option<tower_lsp::lsp_types::GotoDefinitionResponse>> {
|
||||
// let tdp = params.text_document_position_params;
|
||||
// let uri = tdp.text_document.uri;
|
||||
// let pos = tdp.position;
|
||||
//
|
||||
// let guard = self.db.read().await;
|
||||
// // Map URI to current text and index
|
||||
// let Some(fid) = guard.file_db.file_id(uri.as_str()) else { return Ok(None) };
|
||||
// let text = guard.file_db.text(fid).to_string();
|
||||
// let idx = prometeu_analysis::TextIndex::new(&text);
|
||||
// let byte = idx.lsp_to_byte(pos.line, pos.character);
|
||||
// let ident = ident_at(&text, byte);
|
||||
//
|
||||
// if let Some(name) = ident {
|
||||
// if let Some(snap) = &guard.last_good {
|
||||
// let mut hits: Vec<lsp::Location> = Vec::new();
|
||||
// for s in &snap.symbols_flat {
|
||||
// if s.name == name {
|
||||
// hits.push(s.location.clone());
|
||||
// }
|
||||
// }
|
||||
// if !hits.is_empty() {
|
||||
// return Ok(Some(lsp::GotoDefinitionResponse::Array(hits)));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Ok(None)
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // MVP stubs: documentSymbol/workspaceSymbol/definition retornam vazio até PRs seguintes
|
||||
// async fn document_symbol(
|
||||
// &self,
|
||||
// params: tower_lsp::lsp_types::DocumentSymbolParams,
|
||||
// ) -> tower_lsp::jsonrpc::Result<Option<tower_lsp::lsp_types::DocumentSymbolResponse>> {
|
||||
// let uri = params.text_document.uri;
|
||||
// let guard = self.db.read().await;
|
||||
// if let Some(snap) = &guard.last_good {
|
||||
// let mut items: Vec<lsp::SymbolInformation> = Vec::new();
|
||||
// for s in &snap.symbols_flat {
|
||||
// if s.location.uri == uri {
|
||||
// items.push(lsp::SymbolInformation {
|
||||
// name: s.name.clone(),
|
||||
// kind: s.kind,
|
||||
// location: s.location.clone(),
|
||||
// tags: None,
|
||||
// deprecated: None,
|
||||
// container_name: None,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// return Ok(Some(lsp::DocumentSymbolResponse::Flat(items)));
|
||||
// }
|
||||
// Ok(Some(lsp::DocumentSymbolResponse::Flat(vec![])))
|
||||
// }
|
||||
//
|
||||
// // async fn workspace_symbol(
|
||||
// // &self,
|
||||
// // params: lsp::WorkspaceSymbolParams,
|
||||
// // ) -> tower_lsp::jsonrpc::Result<Option<Vec<lsp::SymbolInformation>>> {
|
||||
// // let query = params.query.to_lowercase();
|
||||
// // let guard = self.db.read().await;
|
||||
// // if let Some(snap) = &guard.last_good {
|
||||
// // let mut out: Vec<lsp::SymbolInformation> = Vec::new();
|
||||
// // for s in &snap.symbols_flat {
|
||||
// // if s.name.to_lowercase().contains(&query) {
|
||||
// // out.push(lsp::SymbolInformation {
|
||||
// // name: s.name.clone(),
|
||||
// // kind: s.kind,
|
||||
// // location: s.location.clone(),
|
||||
// // tags: None,
|
||||
// // deprecated: None,
|
||||
// // container_name: None,
|
||||
// // });
|
||||
// // if out.len() >= 50 { break; }
|
||||
// // }
|
||||
// // }
|
||||
// // return Ok(Some(out));
|
||||
// // }
|
||||
// // Ok(Some(vec![]))
|
||||
// // }
|
||||
// }
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// let stdin = tokio::io::stdin();
|
||||
// let stdout = tokio::io::stdout();
|
||||
//
|
||||
// let db: SharedDb = Arc::new(RwLock::new(analysis_db::sourceDb::default()));
|
||||
//
|
||||
// let (service, socket) = LspService::new(|client| Backend { db: db.clone(), client });
|
||||
// Server::new(stdin, stdout, socket).serve(service).await;
|
||||
}
|
||||
|
||||
// // Simple textual identifier extraction for MVP definition lookup.
|
||||
// fn ident_at(text: &str, byte: u32) -> Option<String> {
|
||||
// let b = byte as usize;
|
||||
// if b > text.len() { return None; }
|
||||
// // Expand left and right over identifier characters (ASCII + underscore; acceptable MVP)
|
||||
// let bytes = text.as_bytes();
|
||||
// let mut start = b;
|
||||
// while start > 0 {
|
||||
// let c = bytes[start - 1];
|
||||
// if (c as char).is_ascii_alphanumeric() || c == b'_' { start -= 1; } else { break; }
|
||||
// }
|
||||
// let mut end = b;
|
||||
// while end < bytes.len() {
|
||||
// let c = bytes[end];
|
||||
// if (c as char).is_ascii_alphanumeric() || c == b'_' { end += 1; } else { break; }
|
||||
// }
|
||||
// if start < end { Some(text[start..end].to_string()) } else { None }
|
||||
// }
|
||||
@ -1,147 +0,0 @@
|
||||
// use tokio_util::sync::CancellationToken;
|
||||
// use tower_lsp::Client;
|
||||
//
|
||||
// use crate::source_db::{AnalysisSnapshot, SharedDb};
|
||||
// use tower_lsp::lsp_types as lsp;
|
||||
// use prometeu_core::{FileId, NameInterner, Severity, SymbolKind, TextIndex};
|
||||
//
|
||||
// #[derive(Clone, Debug)]
|
||||
// pub struct LspDiagnostic {
|
||||
// pub range: lsp::Range,
|
||||
// pub severity: Option<lsp::DiagnosticSeverity>,
|
||||
// pub code: Option<lsp::NumberOrString>,
|
||||
// pub message: String,
|
||||
// }
|
||||
//
|
||||
// #[derive(Clone, Debug)]
|
||||
// pub struct FlatSymbol {
|
||||
// pub name: String,
|
||||
// pub kind: lsp::SymbolKind,
|
||||
// pub location: lsp::Location,
|
||||
// }
|
||||
//
|
||||
// /// Requests a project rebuild (coarse). Cancels the previous rebuild if in progress.
|
||||
// pub async fn request_rebuild(db: SharedDb, client: Client) {
|
||||
// // 1) short lock: cancel previous token and install a new one
|
||||
// let new_token = CancellationToken::new();
|
||||
// {
|
||||
// let mut guard = db.write().await;
|
||||
// if let Some(prev) = guard.active_rebuild.take() {
|
||||
// prev.cancel();
|
||||
// }
|
||||
// guard.active_rebuild = Some(new_token.clone());
|
||||
// }
|
||||
//
|
||||
// // 2) spawn task: run analysis outside the lock
|
||||
// tokio::spawn(async move {
|
||||
// // Safe point: check before starting
|
||||
// if new_token.is_cancelled() { return; }
|
||||
//
|
||||
// // Clone snapshot of files (URIs and texts) under a short read lock
|
||||
// let (files, revision) = {
|
||||
// let guard = db.read().await;
|
||||
// let mut v = Vec::new();
|
||||
// for fid in guard.file_ids() {
|
||||
// let uri = guard.file_db.uri(fid).to_string();
|
||||
// let text = guard.file_db.text(fid).to_string();
|
||||
// v.push((fid, uri, text));
|
||||
// }
|
||||
// (v, guard.revision)
|
||||
// };
|
||||
//
|
||||
// // Prepare accumulators
|
||||
// let mut diagnostics_by_uri: std::collections::HashMap<String, Vec<LspDiagnostic>> = std::collections::HashMap::new();
|
||||
// let mut symbols_flat: Vec<FlatSymbol> = Vec::new();
|
||||
//
|
||||
// // For each file: run a minimal frontend to collect diagnostics and top-level symbols
|
||||
// for (fid, uri, text) in files.into_iter() {
|
||||
// if new_token.is_cancelled() { return; }
|
||||
// let text_index = TextIndex::new(&text);
|
||||
//
|
||||
// // Parser + basic pipeline
|
||||
// let mut interner = NameInterner::new();
|
||||
// let mut parser = ParserFacade::new(&text, fid, &mut interner);
|
||||
// match parser.parse_and_collect() {
|
||||
// Ok(parsed) => {
|
||||
// // Diagnostics (from parse/collect are already inside parsed.diags)
|
||||
// let mut file_diags = Vec::new();
|
||||
// for d in parsed.diagnostics {
|
||||
// let range = span_to_range(fid, &text_index, d.span.start, d.span.end);
|
||||
// file_diags.push(LspDiagnostic {
|
||||
// range,
|
||||
// severity: Some(match d.severity { Severity::Error => lsp::DiagnosticSeverity::ERROR, Severity::Warning => lsp::DiagnosticSeverity::WARNING }),
|
||||
// code: Some(lsp::NumberOrString::String(d.code)),
|
||||
// message: d.message,
|
||||
// });
|
||||
// }
|
||||
// diagnostics_by_uri.insert(uri.clone(), file_diags);
|
||||
//
|
||||
// // Symbols: flatten only top-level decls with their decl_span
|
||||
// for sym in parsed.symbols {
|
||||
// let lsp_loc = lsp::Location {
|
||||
// uri: uri.parse().unwrap_or_else(|_| lsp::Url::parse("untitled:").unwrap()),
|
||||
// range: span_to_range(fid, &text_index, sym.decl_span.start, sym.decl_span.end),
|
||||
// };
|
||||
// let kind = match sym.kind {
|
||||
// SymbolKind::Function => lsp::SymbolKind::FUNCTION,
|
||||
// SymbolKind::Service => lsp::SymbolKind::INTERFACE,
|
||||
// SymbolKind::Struct => lsp::SymbolKind::STRUCT,
|
||||
// SymbolKind::Contract => lsp::SymbolKind::CLASS,
|
||||
// SymbolKind::ErrorType => lsp::SymbolKind::ENUM,
|
||||
// _ => lsp::SymbolKind::VARIABLE,
|
||||
// };
|
||||
// symbols_flat.push(FlatSymbol { name: sym.name, kind, location: lsp_loc });
|
||||
// }
|
||||
// }
|
||||
// Err(diags) => {
|
||||
// // Parser returned errors only; publish them
|
||||
// let mut file_diags = Vec::new();
|
||||
// for d in diags {
|
||||
// let range = span_to_range(fid, &text_index, d.span.start, d.span.end);
|
||||
// file_diags.push(LspDiagnostic {
|
||||
// range,
|
||||
// severity: Some(match d.severity { Severity::Error => lsp::DiagnosticSeverity::ERROR, Severity::Warning => lsp::DiagnosticSeverity::WARNING }),
|
||||
// code: Some(lsp::NumberOrString::String(d.code)),
|
||||
// message: d.message,
|
||||
// });
|
||||
// }
|
||||
// diagnostics_by_uri.insert(uri.clone(), file_diags);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if new_token.is_cancelled() { return; }
|
||||
//
|
||||
// // 3) short lock: swap state + revision++ if not cancelled; then publish diagnostics
|
||||
// let snapshot = AnalysisSnapshot { diagnostics_by_uri: diagnostics_by_uri.clone(), symbols_flat };
|
||||
// {
|
||||
// let mut guard = db.write().await;
|
||||
// if new_token.is_cancelled() { return; }
|
||||
// // if no new changes since we started, accept this snapshot
|
||||
// guard.last_good = Some(snapshot);
|
||||
// guard.revision = revision.saturating_add(1);
|
||||
// }
|
||||
//
|
||||
// // Publish diagnostics per file
|
||||
// for (uri, diags) in diagnostics_by_uri.into_iter() {
|
||||
// let lsp_diags: Vec<lsp::Diagnostic> = diags.into_iter().map(|d| lsp::Diagnostic {
|
||||
// range: d.range,
|
||||
// severity: d.severity,
|
||||
// code: d.code,
|
||||
// message: d.message,
|
||||
// ..Default::default()
|
||||
// }).collect();
|
||||
// let _ = client.publish_diagnostics(uri.parse().unwrap_or_else(|_| lsp::Url::parse("untitled:").unwrap()), lsp_diags, None).await;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// fn span_to_range(file: FileId, idx: &TextIndex, start: u32, end: u32) -> lsp::Range {
|
||||
// // Ignore `file` here since idx is built from that file's text
|
||||
// let (s_line, s_col) = idx.byte_to_lsp(start);
|
||||
// let (e_line, e_col) = idx.byte_to_lsp(end);
|
||||
// lsp::Range {
|
||||
// start: lsp::Position { line: s_line, character: s_col },
|
||||
// end: lsp::Position { line: e_line, character: e_col },
|
||||
// }
|
||||
// }
|
||||
@ -1,4 +1,4 @@
|
||||
< [Back](chapter-14.md) | [Summary](table-of-contents.md) >
|
||||
< [Back](chapter-14.md) | [Summary](table-of-contents.md) | [Next](chapter-16.md) >
|
||||
|
||||
# Asset Management
|
||||
|
||||
@ -328,4 +328,4 @@ Where:
|
||||
> **Asset Types describe content.**
|
||||
> **The SDK orchestrates; the hardware executes.**
|
||||
|
||||
< [Back](chapter-14.md) | [Summary](table-of-contents.md) >
|
||||
< [Back](chapter-14.md) | [Summary](table-of-contents.md) | [Next](chapter-16.md) >
|
||||
294
docs/specs/hardware/topics/chapter-16.md
Normal file
294
docs/specs/hardware/topics/chapter-16.md
Normal file
@ -0,0 +1,294 @@
|
||||
< [Back](chapter-15.md) | [Summary](table-of-contents.md) >
|
||||
|
||||
# **Host ABI and Syscalls**
|
||||
|
||||
This chapter defines the Application Binary Interface (ABI) between the Prometeu Virtual Machine (PVM) and the host environment. It specifies how syscalls are encoded, invoked, verified, and accounted for.
|
||||
|
||||
Syscalls provide controlled access to host-managed subsystems such as graphics, audio, input, asset banks, and persistent storage.
|
||||
|
||||
This chapter defines the **contract**. Individual subsystems (GFX, AUDIO, MEMCARD, ASSETS, etc.) define their own syscall tables that conform to this ABI.
|
||||
|
||||
---
|
||||
|
||||
## 1 Design Principles
|
||||
|
||||
The syscall system follows these rules:
|
||||
|
||||
1. **Deterministic**: Syscalls must behave deterministically for the same inputs and frame state.
|
||||
2. **Synchronous**: Syscalls execute to completion within the current VM slice.
|
||||
3. **Non-blocking**: Long operations must be modeled as request + status polling.
|
||||
4. **Capability-gated**: Each syscall requires a declared capability.
|
||||
5. **Stack-based ABI**: Arguments and return values are passed via VM slots.
|
||||
6. **Not first-class**: Syscalls are callable but cannot be stored as values.
|
||||
|
||||
---
|
||||
|
||||
## 2 Syscall Instruction Semantics
|
||||
|
||||
The VM provides a single instruction:
|
||||
|
||||
```
|
||||
SYSCALL <id>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
* `<id>` is a 32-bit integer identifying the syscall.
|
||||
|
||||
Execution steps:
|
||||
|
||||
1. The VM looks up the syscall metadata using `<id>`.
|
||||
2. The VM verifies that enough arguments exist on the stack.
|
||||
3. The VM checks capability requirements.
|
||||
4. The syscall executes in the host environment.
|
||||
5. The syscall leaves exactly `ret_slots` values on the stack.
|
||||
|
||||
If any contract rule is violated, the VM traps.
|
||||
|
||||
---
|
||||
|
||||
## 3 Syscall Metadata Table
|
||||
|
||||
Each syscall is defined by a metadata entry.
|
||||
|
||||
### SyscallMeta structure
|
||||
|
||||
```
|
||||
SyscallMeta {
|
||||
id: u32
|
||||
name: string
|
||||
arg_slots: u8
|
||||
ret_slots: u8
|
||||
capability: CapabilityId
|
||||
may_allocate: bool
|
||||
cost_hint: u32
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
| Field | Description |
|
||||
| -------------- | ------------------------------------------------ |
|
||||
| `id` | Unique syscall identifier |
|
||||
| `name` | Human-readable name |
|
||||
| `arg_slots` | Number of input stack slots |
|
||||
| `ret_slots` | Number of return stack slots |
|
||||
| `capability` | Required capability |
|
||||
| `may_allocate` | Whether the syscall may allocate VM heap objects |
|
||||
| `cost_hint` | Expected cycle cost (for analysis/profiling) |
|
||||
|
||||
The verifier uses this table to validate stack effects.
|
||||
|
||||
---
|
||||
|
||||
## 4 Arguments and Return Values
|
||||
|
||||
Syscalls use the same slot-based ABI as functions.
|
||||
|
||||
### Argument passing
|
||||
|
||||
Arguments are pushed onto the stack before the syscall.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
push a
|
||||
push b
|
||||
SYSCALL X // expects 2 arguments
|
||||
```
|
||||
|
||||
### Return values
|
||||
|
||||
After execution, the syscall leaves exactly `ret_slots` values on the stack.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
// before: []
|
||||
SYSCALL input_state
|
||||
// after: [held, pressed, released]
|
||||
```
|
||||
|
||||
### Slot types
|
||||
|
||||
Each slot contains one of the VM value types:
|
||||
|
||||
* int
|
||||
* bool
|
||||
* float
|
||||
* handle
|
||||
* null
|
||||
|
||||
Composite return values are represented as multiple slots (stack tuples).
|
||||
|
||||
---
|
||||
|
||||
## 5 Syscalls as Callable Entities (Not First-Class)
|
||||
|
||||
Syscalls behave like functions in terms of arguments and return values, but they are **not first-class values**.
|
||||
|
||||
This means:
|
||||
|
||||
* Syscalls can be invoked.
|
||||
* Syscalls cannot be stored in variables.
|
||||
* Syscalls cannot be passed as arguments.
|
||||
* Syscalls cannot be returned from functions.
|
||||
|
||||
Only user-defined functions and closures are first-class.
|
||||
|
||||
### Example declaration (conceptual)
|
||||
|
||||
```
|
||||
host fn input_state() -> (int, int, int)
|
||||
```
|
||||
|
||||
This represents a syscall with three return values, but it cannot be treated as a function value.
|
||||
|
||||
---
|
||||
|
||||
## 6 Error Model: Traps vs Status Codes
|
||||
|
||||
Syscalls use a hybrid error model.
|
||||
|
||||
### Trap conditions (contract violations)
|
||||
|
||||
The VM traps when:
|
||||
|
||||
* The syscall id is invalid.
|
||||
* The required capability is missing.
|
||||
* The stack does not contain enough arguments.
|
||||
* A handle is invalid or dead.
|
||||
|
||||
These are considered fatal contract violations.
|
||||
|
||||
### Status returns (domain conditions)
|
||||
|
||||
Normal operational states are returned as values.
|
||||
|
||||
Examples:
|
||||
|
||||
* asset not yet loaded
|
||||
* audio voice unavailable
|
||||
* memcard full
|
||||
|
||||
These are represented by status codes in return slots.
|
||||
|
||||
---
|
||||
|
||||
## 7 Capability System
|
||||
|
||||
Each syscall requires a capability.
|
||||
|
||||
Capabilities are declared by the cartridge manifest.
|
||||
|
||||
Example capability groups:
|
||||
|
||||
* `gfx`
|
||||
* `audio`
|
||||
* `input`
|
||||
* `asset`
|
||||
* `memcard`
|
||||
|
||||
If a syscall is invoked without the required capability:
|
||||
|
||||
* The VM traps.
|
||||
|
||||
---
|
||||
|
||||
## 8 Interaction with the Garbage Collector
|
||||
|
||||
The VM heap is managed by the GC. Host-managed memory is separate.
|
||||
|
||||
### Heap vs host memory
|
||||
|
||||
| Memory | Managed by | GC scanned |
|
||||
| --------------- | ---------- | ---------- |
|
||||
| VM heap objects | VM GC | Yes |
|
||||
| Asset banks | Host | No |
|
||||
| Audio buffers | Host | No |
|
||||
| Framebuffers | Host | No |
|
||||
|
||||
Assets are addressed by identifiers, not VM heap handles.
|
||||
|
||||
### Host root rule
|
||||
|
||||
If a syscall stores a handle to a VM heap object beyond the duration of the call, it must register that handle as a **host root**.
|
||||
|
||||
This prevents the GC from collecting objects still in use by the host.
|
||||
|
||||
This rule applies only to VM heap objects (such as closures or user objects), not to asset identifiers or primitive values.
|
||||
|
||||
---
|
||||
|
||||
## 9 Determinism Rules
|
||||
|
||||
Syscalls must obey deterministic execution rules.
|
||||
|
||||
Forbidden behaviors:
|
||||
|
||||
* reading real-time clocks
|
||||
* accessing non-deterministic OS APIs
|
||||
* performing blocking I/O
|
||||
|
||||
Allowed patterns:
|
||||
|
||||
* frame-based timers
|
||||
* request + poll status models
|
||||
* event delivery at frame boundaries
|
||||
|
||||
---
|
||||
|
||||
## 10 Cost Model and Budgeting
|
||||
|
||||
Each syscall contributes to frame cost.
|
||||
|
||||
The VM tracks:
|
||||
|
||||
* cycles spent in syscalls
|
||||
* syscall counts
|
||||
* allocation cost (if any)
|
||||
|
||||
Example telemetry:
|
||||
|
||||
```
|
||||
Frame 10231:
|
||||
Syscalls: 12
|
||||
Cycles (syscalls): 380
|
||||
Allocations via syscalls: 2
|
||||
```
|
||||
|
||||
Nothing is free.
|
||||
|
||||
---
|
||||
|
||||
## 11 Blocking and Long Operations
|
||||
|
||||
Syscalls must not block.
|
||||
|
||||
Long operations must use a two-phase model:
|
||||
|
||||
1. Request
|
||||
2. Status polling or event notification
|
||||
|
||||
Example pattern:
|
||||
|
||||
```
|
||||
asset.load(id)
|
||||
...
|
||||
status, progress = asset.status(id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12 Summary
|
||||
|
||||
* Syscalls are deterministic, synchronous, and non-blocking.
|
||||
* They use the same slot-based ABI as functions.
|
||||
* They are callable but not first-class.
|
||||
* Capabilities control access to host subsystems.
|
||||
* GC only manages VM heap objects.
|
||||
* Host-held heap objects must be registered as roots.
|
||||
* All syscall costs are tracked per frame.
|
||||
|
||||
|
||||
< [Back](chapter-15.md) | [Summary](table-of-contents.md) >
|
||||
@ -1,457 +1,427 @@
|
||||
< [Back](chapter-1.md) | [Summary](table-of-contents.md) | [Next](chapter-3.md) >
|
||||
|
||||
# ⚙️ ** PVM (PROMETEU VM) — Instruction Set**
|
||||
# **Prometeu Virtual Machine (PVM)**
|
||||
|
||||
## 1. Overview
|
||||
This chapter defines the execution model, value system, calling convention, memory model, and host interface of the Prometeu Virtual Machine.
|
||||
|
||||
The **PROMETEU VM** is a mandatory virtual machine always present in the logical hardware:
|
||||
|
||||
* **stack-based**
|
||||
* deterministic
|
||||
* cycle-oriented
|
||||
* designed for teaching and inspection
|
||||
|
||||
It exists to:
|
||||
|
||||
* map high-level language concepts
|
||||
* make computational cost visible
|
||||
* allow execution analysis
|
||||
* serve as the foundation of the PROMETEU cartridge
|
||||
|
||||
> The PROMETEU VM is simple by choice.
|
||||
> Simplicity is a pedagogical tool.
|
||||
The PVM is a **deterministic, stack-based VM** designed for a 2D fantasy console environment. Its primary goal is to provide predictable performance, safe memory access, and a stable execution contract suitable for real-time games running at a fixed frame rate. fileciteturn2file0
|
||||
|
||||
---
|
||||
|
||||
## 2. Execution Model
|
||||
## 1 Core Design Principles
|
||||
|
||||
### 2.1 Main Components
|
||||
The PVM is designed around the following constraints:
|
||||
|
||||
The VM has:
|
||||
|
||||
* **PC (Program Counter)** — next instruction
|
||||
* **Operand Stack** — value stack
|
||||
* **Call Stack** — stores execution frames for function calls
|
||||
* **Scope Stack** — stores frames for blocks within a function
|
||||
* **Heap** — dynamic memory
|
||||
* **Globals** — global variables
|
||||
* **Constant Pool** — literals and references
|
||||
* **ROM** — cartridge bytecode
|
||||
* **RAM** — mutable data
|
||||
1. **Deterministic execution**: no hidden threads or asynchronous callbacks.
|
||||
2. **Frame-based timing**: execution is bounded by frame time.
|
||||
3. **Safe memory model**: all heap objects are accessed through handles.
|
||||
4. **Simple compilation target**: stack-based bytecode with verified control flow.
|
||||
5. **Stable ABI**: multi-value returns with fixed slot semantics.
|
||||
6. **First-class functions**: functions can be passed, stored, and returned.
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Execution Cycle
|
||||
## 2 Execution Model
|
||||
|
||||
Each instruction executes:
|
||||
The PVM executes bytecode in a **frame loop**. Each frame:
|
||||
|
||||
```
|
||||
FETCH → DECODE → EXECUTE → ADVANCE PC
|
||||
```
|
||||
1. The firmware enters the VM.
|
||||
2. The VM runs until:
|
||||
|
||||
Properties:
|
||||
* the frame budget is consumed, or
|
||||
* a `FRAME_SYNC` instruction is reached.
|
||||
3. At `FRAME_SYNC`:
|
||||
|
||||
* every instruction has a fixed cost in cycles
|
||||
* there is no invisible implicit work
|
||||
* execution is fully deterministic
|
||||
* events are delivered
|
||||
* input is sampled
|
||||
* optional GC may run
|
||||
4. Control returns to the firmware.
|
||||
|
||||
`FRAME_SYNC` is the **primary safepoint** in the system.
|
||||
|
||||
---
|
||||
|
||||
## 3. Fundamental Types
|
||||
## 3 Value Types
|
||||
|
||||
All runtime values are stored in VM slots as a `Value`.
|
||||
|
||||
### Primitive value types (stack values)
|
||||
|
||||
| Type | Description |
|
||||
| --------- | ------------------------- |
|
||||
| `int32` | 32-bit signed integer |
|
||||
| `int64` | 64-bit signed integer |
|
||||
| ------- | --------------------- |
|
||||
| `int` | 64-bit signed integer |
|
||||
| `bool` | Boolean value |
|
||||
| `float` | 64-bit floating point |
|
||||
| `boolean` | true/false |
|
||||
| `string` | immutable UTF-8 |
|
||||
| `null` | absence of value |
|
||||
| `ref` | heap reference |
|
||||
|
||||
### 3.1 Numeric Promotion
|
||||
The VM promotes types automatically during operations:
|
||||
* `int32` + `int32` → `int32`
|
||||
* `int32` + `int64` → `int64`
|
||||
* `int` + `float` → `float`
|
||||
* Bitwise operations promote `int32` to `int64` if any operand is `int64`.
|
||||
### Built-in vector and graphics types (stack values)
|
||||
|
||||
Do not exist:
|
||||
These are treated as VM primitives with dedicated opcodes:
|
||||
|
||||
* magic coercions
|
||||
* implicit casts
|
||||
* silent overflows
|
||||
| Type | Description |
|
||||
| ------- | --------------------------------- |
|
||||
| `vec2` | 2D vector (x, y) |
|
||||
| `color` | Packed color value |
|
||||
| `pixel` | Combination of position and color |
|
||||
|
||||
These types:
|
||||
|
||||
* live entirely on the stack
|
||||
* are copied by value
|
||||
* never allocate on the heap
|
||||
|
||||
### Heap values
|
||||
|
||||
All user-defined objects live on the heap and are accessed via **handles**.
|
||||
|
||||
| Type | Description |
|
||||
| -------- | -------------------------- |
|
||||
| `handle` | Reference to a heap object |
|
||||
| `null` | Null handle |
|
||||
|
||||
Handles may refer to:
|
||||
|
||||
* user objects
|
||||
* arrays
|
||||
* strings
|
||||
* closures
|
||||
|
||||
---
|
||||
|
||||
## 4. Stack Conventions & Calling ABI
|
||||
## 4 Handles and Gate Table
|
||||
|
||||
* Operations use the top of the stack.
|
||||
* Results always return to the stack.
|
||||
* **LIFO Order:** Last pushed = first consumed.
|
||||
* **Mandatory Return:** Every function (`Call`) and `Syscall` MUST leave exactly **one** value on the stack upon completion. If there is no meaningful value to return, `Null` must be pushed.
|
||||
|
||||
### 4.1 Calling Convention (Call / Ret)
|
||||
|
||||
1. **Arguments:** The caller pushes arguments in order (arg0, arg1, ..., argN-1).
|
||||
2. **Execution:** The `Call` instruction specifies `args_count`. These `N` values become the **locals** of the new frame (local 0 = arg0, local 1 = arg1, etc.).
|
||||
3. **Return Value:** Before executing `Ret`, the callee MUST push its return value.
|
||||
4. **Cleanup:** The `Ret` instruction is responsible for:
|
||||
- Popping the return value.
|
||||
- Removing all locals (the arguments) from the operand stack.
|
||||
- Re-pushing the return value.
|
||||
- Restoring the previous frame and PC.
|
||||
|
||||
### 4.2 Syscall Convention
|
||||
|
||||
1. **Arguments:** The caller pushes arguments in order.
|
||||
2. **Execution:** The native implementation pops arguments as needed. Since it's a stack, it will pop them in reverse order (argN-1 first, then argN-2, ..., arg0).
|
||||
3. **Return Value:** The native implementation MUST push exactly one value onto the stack before returning to the VM.
|
||||
4. **Cleanup:** The native implementation is responsible for popping all arguments it expects.
|
||||
|
||||
Example:
|
||||
Heap objects are accessed through **handles**. A handle is a pair:
|
||||
|
||||
```
|
||||
PUSH_CONST 3
|
||||
PUSH_CONST 4
|
||||
ADD
|
||||
handle = { index, generation }
|
||||
```
|
||||
|
||||
State:
|
||||
The VM maintains a **gate table**:
|
||||
|
||||
```
|
||||
[3]
|
||||
[3, 4]
|
||||
[7]
|
||||
GateEntry {
|
||||
alive: bool
|
||||
generation: u32
|
||||
base: usize
|
||||
slots: u32
|
||||
type_id: u32
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
When an object is freed:
|
||||
|
||||
## 5. Instruction Categories
|
||||
* its gate entry is marked dead
|
||||
* its generation is incremented
|
||||
|
||||
1. Flow control
|
||||
2. Stack
|
||||
3. Arithmetic and logic
|
||||
4. Variables
|
||||
5. Functions
|
||||
6. Heap and structures
|
||||
7. Peripherals (syscalls)
|
||||
8. System
|
||||
If a handle’s generation does not match the gate entry, the VM traps.
|
||||
|
||||
This prevents use-after-free bugs.
|
||||
|
||||
---
|
||||
|
||||
## 6. Instructions — VM Set 1
|
||||
## 5 Heap Model
|
||||
|
||||
### 6.1 Execution Control
|
||||
* All user objects live in the heap.
|
||||
* Objects are fixed-layout blocks of slots.
|
||||
* No inheritance at the memory level.
|
||||
* Traits/interfaces are resolved by the compiler or via vtables.
|
||||
|
||||
| Instruction | Cycles | Description |
|
||||
| ------------------- | ------ | ------------------------- |
|
||||
| `NOP` | 1 | Does nothing |
|
||||
| `HALT` | 1 | Terminates execution |
|
||||
| `JMP addr` | 2 | Unconditional jump |
|
||||
| `JMP_IF_FALSE addr` | 3 | Jumps if top is false |
|
||||
| `JMP_IF_TRUE addr` | 3 | Jumps if top is true |
|
||||
Built-in types remain stack-only.
|
||||
|
||||
Heap objects include:
|
||||
|
||||
* user structs/classes
|
||||
* strings
|
||||
* arrays
|
||||
* closures
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Stack
|
||||
## 6 Tuples and Multi-Return ABI
|
||||
|
||||
| Instruction | Cycles | Description |
|
||||
| -------------- | ------ | ------------------- |
|
||||
| `PUSH_CONST k` | 2 | Pushes constant |
|
||||
| `POP` | 1 | Removes top |
|
||||
| `DUP` | 1 | Duplicates top |
|
||||
| `SWAP` | 1 | Swaps two tops |
|
||||
| `PUSH_I32 v` | 2 | Pushes 32-bit int |
|
||||
| `PUSH_I64 v` | 2 | Pushes 64-bit int |
|
||||
| `PUSH_F64 v` | 2 | Pushes 64-bit float |
|
||||
| `PUSH_BOOL v` | 2 | Pushes boolean |
|
||||
The PVM supports **multi-value returns**.
|
||||
|
||||
### Tuple rules
|
||||
|
||||
* Tuples are **stack-only**.
|
||||
* Maximum tuple arity is **N = 6**.
|
||||
* Tuples are not heap objects by default.
|
||||
* To persist a tuple, it must be explicitly boxed.
|
||||
|
||||
### Call convention
|
||||
|
||||
Each function declares a fixed `ret_slots` value.
|
||||
|
||||
At call time:
|
||||
|
||||
1. Caller prepares arguments.
|
||||
2. `CALL` transfers control.
|
||||
3. Callee executes.
|
||||
4. `RET` leaves exactly `ret_slots` values on the stack.
|
||||
|
||||
The verifier ensures that:
|
||||
|
||||
* all control paths produce the same `ret_slots`
|
||||
* stack depth is consistent.
|
||||
|
||||
---
|
||||
|
||||
### 6.3 Arithmetic
|
||||
## 7 Call Stack and Frames
|
||||
|
||||
| Instruction | Cycles |
|
||||
| ----------- | ------ |
|
||||
| `ADD` | 2 |
|
||||
| `SUB` | 2 |
|
||||
| `MUL` | 4 |
|
||||
| `DIV` | 6 |
|
||||
The VM uses a **call stack**.
|
||||
|
||||
---
|
||||
|
||||
### 6.4 Comparison and Logic
|
||||
|
||||
| Instruction | Cycles |
|
||||
| ----------- | ------ |
|
||||
| `EQ` | 2 |
|
||||
| `NEQ` | 2 |
|
||||
| `LT` | 2 |
|
||||
| `GT` | 2 |
|
||||
| `LTE` | 2 |
|
||||
| `GTE` | 2 |
|
||||
| `AND` | 2 |
|
||||
| `OR` | 2 |
|
||||
| `NOT` | 1 |
|
||||
| `BIT_AND` | 2 |
|
||||
| `BIT_OR` | 2 |
|
||||
| `BIT_XOR` | 2 |
|
||||
| `SHL` | 2 |
|
||||
| `SHR` | 2 |
|
||||
| `NEG` | 1 |
|
||||
|
||||
---
|
||||
|
||||
### 6.5 Variables
|
||||
|
||||
| Instruction | Cycles | Description |
|
||||
| -------------- | ------ | ---------------- |
|
||||
| `GET_GLOBAL i` | 3 | Reads global |
|
||||
| `SET_GLOBAL i` | 3 | Writes global |
|
||||
| `GET_LOCAL i` | 2 | Reads local |
|
||||
| `SET_LOCAL i` | 2 | Writes local |
|
||||
|
||||
---
|
||||
|
||||
### 6.6 Functions
|
||||
|
||||
| Instruction | Cycles | Description |
|
||||
|----------------------| ------ |-----------------------------------------------|
|
||||
| `CALL <u32 func_id>` | 5 | Saves PC and creates a new call frame |
|
||||
| `RET` | 4 | Returns from function, restoring PC |
|
||||
| `PUSH_SCOPE` | 3 | Creates a scope within the current function |
|
||||
| `POP_SCOPE` | 3 | Removes current scope and its local variables |
|
||||
|
||||
**ABI Rules for Functions:**
|
||||
* **`func_id`:** A 32-bit index into the **final FunctionTable**, assigned by the compiler linker at build time.
|
||||
* **Mandatory Return Value:** Every function MUST leave exactly one value on the stack before `RET`. If the function logic doesn't return a value, it must push `null`.
|
||||
* **Stack Cleanup:** `RET` automatically clears all local variables (based on `stack_base`) and re-pushes the return value.
|
||||
|
||||
---
|
||||
|
||||
### 6.7 Heap
|
||||
|
||||
| Instruction | Cycles | Description |
|
||||
| --------------- | ------ | --------------- |
|
||||
| `ALLOC size` | 10 | Allocates on heap |
|
||||
| `LOAD_REF off` | 3 | Reads field |
|
||||
| `STORE_REF off` | 3 | Writes field |
|
||||
|
||||
Heap is:
|
||||
|
||||
* finite
|
||||
* monitored
|
||||
* accounted for in the CAP
|
||||
|
||||
---
|
||||
|
||||
### 6.8 Peripherals (Syscalls)
|
||||
|
||||
| Instruction | Cycles | Description |
|
||||
|--------------| -------- | --------------------- |
|
||||
| `SYSCALL id` | variable | Call to hardware |
|
||||
|
||||
**ABI Rules for Syscalls:**
|
||||
* **Argument Order:** Arguments must be pushed in the order they appear in the call (LIFO stack behavior).
|
||||
* Example: `gfx.draw_rect(x, y, w, h, color)` means:
|
||||
1. `PUSH x`
|
||||
2. `PUSH y`
|
||||
3. `PUSH w`
|
||||
4. `PUSH h`
|
||||
5. `PUSH color`
|
||||
6. `SYSCALL 0x1002`
|
||||
* **Consumption:** The native function MUST pop all its arguments from the stack.
|
||||
* **Return Value:** If the syscall returns a value, it will be pushed onto the stack by the native implementation. If not, the stack state for the caller remains as it was before pushing arguments.
|
||||
|
||||
#### Implemented Syscalls (v0.1)
|
||||
|
||||
| ID | Name | Arguments (Stack) | Return |
|
||||
| ---------- | ----------------- | ---------------------------- | ------- |
|
||||
| `0x0001` | `system.has_cart` | - | `bool` |
|
||||
| `0x0002` | `system.run_cart` | - | - |
|
||||
| `0x1001` | `gfx.clear` | `color_idx` | - |
|
||||
| `0x1002` | `gfx.draw_rect` | `x, y, w, h, color_idx` | - |
|
||||
| `0x1003` | `gfx.draw_line` | `x1, y1, x2, y2, color_idx` | - |
|
||||
| `0x1004` | `gfx.draw_circle` | `xc, yc, r, color_idx` | - |
|
||||
| `0x1005` | `gfx.draw_disc` | `xc, yc, r, b_col, f_col` | - |
|
||||
| `0x1006` | `gfx.draw_square` | `x, y, w, h, b_col, f_col` | - |
|
||||
| `0x2001` | `input.get_pad` | `button_id` | `bool` |
|
||||
| `0x3001` | `audio.play` | `s_id, v_id, vol, pan, pitch`| - |
|
||||
|
||||
**Button IDs:**
|
||||
- `0`: Up, `1`: Down, `2`: Left, `3`: Right
|
||||
- `4`: A, `5`: B, `6`: X, `7`: Y
|
||||
- `8`: L, `9`: R
|
||||
- `10`: Start, `11`: Select
|
||||
|
||||
---
|
||||
|
||||
## 7. Execution Errors
|
||||
|
||||
Errors are:
|
||||
|
||||
* explicit
|
||||
* fatal
|
||||
* never silent
|
||||
|
||||
Types:
|
||||
|
||||
* stack underflow
|
||||
* invalid type
|
||||
* invalid heap
|
||||
* invalid frame
|
||||
|
||||
Generate:
|
||||
|
||||
* clear message
|
||||
* state dump
|
||||
* stack trace
|
||||
|
||||
---
|
||||
|
||||
## 8. Determinism
|
||||
|
||||
Guarantees:
|
||||
|
||||
* same input → same result
|
||||
* same sequence → same cycles
|
||||
* no speculative execution
|
||||
* no invisible optimizations
|
||||
|
||||
> If you see the instruction, you pay for it.
|
||||
|
||||
---
|
||||
|
||||
## 9. Relationship with Languages
|
||||
|
||||
Java, TypeScript, Lua etc:
|
||||
|
||||
* are source languages
|
||||
* compiled to this bytecode
|
||||
* never executed directly
|
||||
|
||||
All run on the **same VM**.
|
||||
|
||||
---
|
||||
|
||||
## 10. Example
|
||||
|
||||
Source:
|
||||
|
||||
```java
|
||||
x = 3 + 4;
|
||||
```
|
||||
|
||||
Bytecode:
|
||||
Each frame contains:
|
||||
|
||||
```
|
||||
PUSH_CONST 3
|
||||
PUSH_CONST 4
|
||||
ADD
|
||||
SET_GLOBAL 0
|
||||
Frame {
|
||||
return_pc
|
||||
base_pointer
|
||||
ret_slots
|
||||
}
|
||||
```
|
||||
|
||||
Cost:
|
||||
Execution uses only the following call instructions:
|
||||
|
||||
```
|
||||
2 + 2 + 2 + 3 = 9 cycles
|
||||
```
|
||||
| Opcode | Description |
|
||||
| ------ | ---------------------- |
|
||||
| `CALL` | Calls a function by id |
|
||||
| `RET` | Returns from function |
|
||||
|
||||
There is no separate `PUSH_FRAME` or `POP_FRAME` instruction in the public ISA.
|
||||
|
||||
---
|
||||
|
||||
## 11. Execution per Tick
|
||||
## 8 Closures and First-Class Functions
|
||||
|
||||
The VM does not run infinitely.
|
||||
Closures are heap objects and represent **function values**.
|
||||
|
||||
It executes:
|
||||
The PVM treats functions as **first-class values**. This means:
|
||||
|
||||
* until consuming the **logical frame** budget
|
||||
* or until `HALT`
|
||||
* Functions can be stored in variables.
|
||||
* Functions can be passed as arguments.
|
||||
* Functions can be returned from other functions.
|
||||
* All function values are represented as closures.
|
||||
|
||||
The budget is defined by the PROMETEU logical hardware (e.g., `CYCLES_PER_FRAME`).
|
||||
Even functions without captures are represented as closures with an empty capture set.
|
||||
|
||||
Example:
|
||||
### Closure layout
|
||||
|
||||
```
|
||||
vm.step_budget(10_000)
|
||||
Closure {
|
||||
func_id
|
||||
captures[]
|
||||
}
|
||||
```
|
||||
|
||||
This feeds:
|
||||
Captures are stored as handles or value copies.
|
||||
|
||||
* CAP
|
||||
* profiling
|
||||
* certification
|
||||
All closure environments are part of the GC root set.
|
||||
|
||||
### Direct and indirect calls
|
||||
|
||||
The PVM supports two forms of function invocation:
|
||||
|
||||
| Opcode | Description |
|
||||
| -------------- | -------------------------------------- |
|
||||
| `CALL` | Direct call by function id |
|
||||
| `CALL_CLOSURE` | Indirect call through a closure handle |
|
||||
|
||||
For `CALL_CLOSURE`:
|
||||
|
||||
1. The closure handle is read from the stack.
|
||||
2. The VM extracts the `func_id` from the closure.
|
||||
3. The function is invoked using the closure’s captures as its environment.
|
||||
|
||||
The verifier ensures that:
|
||||
|
||||
* The closure handle is valid.
|
||||
* The target function’s arity matches the call site.
|
||||
* The `ret_slots` contract is respected.
|
||||
|
||||
---
|
||||
|
||||
## 12. Logical Frame and `FRAME_SYNC`
|
||||
## 9 Coroutines (Deterministic)
|
||||
|
||||
PROMETEU defines **logical frame** as the minimum unit of consistent game update.
|
||||
The PVM supports **cooperative coroutines**.
|
||||
|
||||
* **Input is latched per logical frame** (does not change until the logical frame is completed).
|
||||
* The **cycle budget** is applied to the logical frame.
|
||||
* A new logical frame only starts when the current frame ends.
|
||||
Characteristics:
|
||||
|
||||
### 12.1 System Instruction: `FRAME_SYNC`
|
||||
* Coroutines are scheduled deterministically.
|
||||
* No preemption.
|
||||
* No parallel execution.
|
||||
* All scheduling happens at safepoints.
|
||||
|
||||
The `FRAME_SYNC` instruction marks the **end of the logical frame**.
|
||||
Each coroutine contains:
|
||||
|
||||
| Instruction | Cycles | Description |
|
||||
| ------------ | ------ | ---------------------------------- |
|
||||
| `FRAME_SYNC` | 1 | Finalizes the current logical frame |
|
||||
```
|
||||
Coroutine {
|
||||
call_stack
|
||||
operand_stack
|
||||
state
|
||||
}
|
||||
```
|
||||
|
||||
Properties:
|
||||
### Coroutine instructions
|
||||
|
||||
* `FRAME_SYNC` is a **system instruction**.
|
||||
* It should not be exposed as a "manual" API to the user.
|
||||
* Tooling/compiler can **inject** `FRAME_SYNC` automatically at the end of the main loop.
|
||||
| Opcode | Description |
|
||||
| ------- | -------------------------- |
|
||||
| `SPAWN` | Creates a coroutine |
|
||||
| `YIELD` | Suspends current coroutine |
|
||||
| `SLEEP` | Suspends for N frames |
|
||||
|
||||
### 12.2 Semantics (what happens when it executes)
|
||||
Scheduling is:
|
||||
|
||||
When executing `FRAME_SYNC`, the core:
|
||||
* round-robin
|
||||
* deterministic
|
||||
* bounded by frame budget
|
||||
|
||||
1. **Finalizes** the current logical frame.
|
||||
2. **Presents** the frame (`gfx.present()` or `gfx.compose_and_present()` depending on the GFX model).
|
||||
3. **Releases** the input latch.
|
||||
4. **Resets** the budget for the next logical frame.
|
||||
|
||||
### 12.3 Overbudget (when the frame doesn't finish on time)
|
||||
|
||||
If the logical frame budget runs out **before** the VM reaches `FRAME_SYNC`:
|
||||
|
||||
* the VM **pauses** (PC and stacks remain at the exact point)
|
||||
* there is **no present**
|
||||
* the input latch is **maintained**
|
||||
* on the next host tick, the VM **continues** from where it left off, still in the same logical frame
|
||||
|
||||
Practical effect:
|
||||
|
||||
* if the code needs 2 budgets to reach `FRAME_SYNC`, the game updates at ~30 FPS (logical frame takes 2 ticks)
|
||||
* this is deterministic and reportable in the CAP
|
||||
Coroutine stacks are part of the GC root set.
|
||||
|
||||
---
|
||||
|
||||
## 15. Extensibility
|
||||
## 10 Garbage Collection
|
||||
|
||||
The Instruction Set is versioned.
|
||||
The PVM uses a **mark-sweep collector**.
|
||||
|
||||
Future:
|
||||
### GC rules
|
||||
|
||||
* DMA
|
||||
* streaming
|
||||
* vectors
|
||||
* fictitious coprocessors
|
||||
* GC runs only at **safepoints**.
|
||||
* The primary safepoint is `FRAME_SYNC`.
|
||||
* GC is triggered by:
|
||||
|
||||
No existing instruction changes its meaning.
|
||||
* heap thresholds, or
|
||||
* allocation pressure.
|
||||
|
||||
### Root set
|
||||
|
||||
The GC marks from:
|
||||
|
||||
* operand stack
|
||||
* call stack frames
|
||||
* global variables
|
||||
* coroutine stacks
|
||||
* closure environments
|
||||
* host-held handles
|
||||
|
||||
The collector:
|
||||
|
||||
* does not compact memory (v1)
|
||||
* uses free lists for reuse
|
||||
|
||||
---
|
||||
|
||||
## 16. Summary
|
||||
## 11 Event and Interrupt Model
|
||||
|
||||
The PVM does not allow asynchronous callbacks.
|
||||
|
||||
All events are:
|
||||
|
||||
* queued by the firmware
|
||||
* delivered at `FRAME_SYNC`
|
||||
|
||||
This ensures:
|
||||
|
||||
* stack-based VM
|
||||
* explicit cost
|
||||
* deterministic execution
|
||||
* integrated with CAP
|
||||
* foundation of every PROMETEU cartridge
|
||||
* predictable frame timing
|
||||
|
||||
Coroutines are the only supported concurrency mechanism.
|
||||
|
||||
---
|
||||
|
||||
## 12 Host Interface (Syscalls)
|
||||
|
||||
All hardware access occurs through syscalls.
|
||||
|
||||
Syscalls are:
|
||||
|
||||
* synchronous
|
||||
* deterministic
|
||||
* capability-checked
|
||||
|
||||
They operate on the following subsystems:
|
||||
|
||||
### Graphics
|
||||
|
||||
* tilebanks
|
||||
* layers
|
||||
* sprites
|
||||
* palette control
|
||||
* fade registers
|
||||
* frame present
|
||||
|
||||
### Audio
|
||||
|
||||
* voice allocation
|
||||
* play/stop
|
||||
* volume/pan/pitch
|
||||
* steal policy
|
||||
|
||||
### Input
|
||||
|
||||
* sampled once per frame
|
||||
* exposed as frame state
|
||||
|
||||
### Assets
|
||||
|
||||
Asset banks are **host-owned memory**.
|
||||
|
||||
The VM interacts through handles:
|
||||
|
||||
| Syscall | Description |
|
||||
| -------------- | ---------------------------- |
|
||||
| `asset.load` | Request asset load into slot |
|
||||
| `asset.status` | Query load state |
|
||||
| `asset.commit` | Activate loaded asset |
|
||||
|
||||
Asset memory:
|
||||
|
||||
* is not part of the VM heap
|
||||
* is not scanned by GC
|
||||
|
||||
### Save Memory (MEMCARD)
|
||||
|
||||
| Syscall | Description |
|
||||
| --------------- | --------------- |
|
||||
| `mem.read_all` | Read save data |
|
||||
| `mem.write_all` | Write save data |
|
||||
| `mem.commit` | Persist save |
|
||||
| `mem.size` | Query capacity |
|
||||
|
||||
---
|
||||
|
||||
## 13 Verifier Requirements
|
||||
|
||||
Before execution, bytecode must pass the verifier.
|
||||
|
||||
The verifier ensures:
|
||||
|
||||
1. Valid jump targets
|
||||
2. Stack depth consistency
|
||||
3. Correct `ret_slots` across all paths
|
||||
4. Handle safety rules
|
||||
5. Closure call safety
|
||||
6. No invalid opcode sequences
|
||||
|
||||
Invalid bytecode is rejected.
|
||||
|
||||
---
|
||||
|
||||
## 14 Summary
|
||||
|
||||
The Prometeu VM is:
|
||||
|
||||
* stack-based
|
||||
* deterministic
|
||||
* frame-synchronized
|
||||
* handle-based for heap access
|
||||
* multi-return capable
|
||||
* first-class function capable
|
||||
* coroutine-driven for concurrency
|
||||
|
||||
This design balances:
|
||||
|
||||
* ease of compilation
|
||||
* predictable performance
|
||||
* safety and debuggability
|
||||
* suitability for real-time 2D games.
|
||||
|
||||
< [Back](chapter-1.md) | [Summary](table-of-contents.md) | [Next](chapter-3.md) >
|
||||
@ -1,309 +1,353 @@
|
||||
< [Back](chapter-2.md) | [Summary](table-of-contents.md) | [Next](chapter-4.md) >
|
||||
|
||||
# 🧠 **Memory: Stack, Heap, and Allocation**
|
||||
# 🧠 **Memory Model**
|
||||
|
||||
## 1. Overview
|
||||
This chapter defines the memory architecture of the Prometeu Virtual Machine (PVM). It describes the stack, heap, handles, object layout, garbage collection, and interaction with host-owned memory such as asset banks.
|
||||
|
||||
PROMETEU treats **memory as an explicit resource**.
|
||||
The memory model is designed to be:
|
||||
|
||||
Nothing is allocated "for convenience".
|
||||
|
||||
Nothing grows "automatically".
|
||||
|
||||
Nothing is invisible.
|
||||
|
||||
This chapter defines:
|
||||
|
||||
- the **memory spaces** of the PROMETEU VM
|
||||
- how **Stack** and **Heap** work
|
||||
- the cost and consequences of **dynamic allocation**
|
||||
- how memory relates to **CAP and certification**
|
||||
* deterministic
|
||||
* safe
|
||||
* simple to verify
|
||||
* suitable for real-time 2D games
|
||||
|
||||
---
|
||||
|
||||
## 2. PROMETEU VM Memory Spaces
|
||||
## 1 Overview
|
||||
|
||||
The PROMETEU VM memory is divided into regions with clear responsibilities:
|
||||
The PVM uses a **split memory model**:
|
||||
|
||||
```
|
||||
+---------------------------+
|
||||
|Constant Pool |
|
||||
+---------------------------+
|
||||
|Global Space |
|
||||
+---------------------------+
|
||||
|Call Stack |
|
||||
| (Frames + Locals) |
|
||||
+---------------------------+
|
||||
| Operand Stack |
|
||||
+---------------------------+
|
||||
| Heap |
|
||||
+---------------------------+
|
||||
1. **Stack memory**
|
||||
|
||||
```
|
||||
* used for temporary values
|
||||
* function arguments
|
||||
* multi-return tuples
|
||||
2. **Heap memory**
|
||||
|
||||
Each region:
|
||||
* used for all user-defined objects
|
||||
* accessed only through handles
|
||||
3. **Host-owned memory**
|
||||
|
||||
- has its own semantics
|
||||
- has defined limits
|
||||
- has a direct impact on execution cost
|
||||
* asset banks
|
||||
* audio buffers
|
||||
* framebuffers
|
||||
* not part of the VM heap
|
||||
|
||||
---
|
||||
|
||||
## 3. Operand Stack
|
||||
## 2 Stack Memory
|
||||
|
||||
### 3.1 Definition
|
||||
The stack is used for:
|
||||
|
||||
The **Operand Stack** is used for:
|
||||
* primitive values
|
||||
* built-in value types
|
||||
* temporary results
|
||||
* function arguments
|
||||
* tuple returns
|
||||
|
||||
- passing operands between instructions
|
||||
- intermediate results
|
||||
- expression evaluation
|
||||
### Stack value types
|
||||
|
||||
It is:
|
||||
| Type | Description |
|
||||
| -------- | ------------------------ |
|
||||
| `int` | 64-bit integer |
|
||||
| `bool` | Boolean |
|
||||
| `float` | 64-bit float |
|
||||
| `vec2` | 2D vector |
|
||||
| `color` | Packed color |
|
||||
| `pixel` | Position + color |
|
||||
| `handle` | Reference to heap object |
|
||||
|
||||
- **LIFO**
|
||||
- automatically growing within the frame
|
||||
- **reset** between frames (except explicitly persisted values)
|
||||
All stack values are:
|
||||
|
||||
* fixed-size
|
||||
* copied by value
|
||||
* never directly reference raw memory
|
||||
|
||||
### Stack properties
|
||||
|
||||
* Stack is bounded and verified.
|
||||
* Stack depth must be consistent across all control paths.
|
||||
* Stack never stores raw pointers.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Characteristics
|
||||
## 3 Tuples (Stack-Only Aggregates)
|
||||
|
||||
- does not store complex structures
|
||||
- stores primitive values or references
|
||||
- overflow or underflow are **fatal errors**
|
||||
Tuples are used for multi-value returns.
|
||||
|
||||
> The operand stack is cheap, fast, and temporary.
|
||||
>
|
||||
### Tuple rules
|
||||
|
||||
---
|
||||
* Tuples exist only on the stack.
|
||||
* Maximum tuple arity: **6 slots**.
|
||||
* Tuples are not heap objects by default.
|
||||
* To persist a tuple, it must be explicitly boxed into a heap object.
|
||||
|
||||
## 4. Call Stack
|
||||
### Example
|
||||
|
||||
### 4.1 Execution Frames
|
||||
|
||||
Each function call creates a **Call Frame**, containing:
|
||||
|
||||
- local variables
|
||||
- parameters
|
||||
- return address
|
||||
- execution context
|
||||
|
||||
Frames are created with:
|
||||
Function returning two values:
|
||||
|
||||
```
|
||||
PUSH_FRAME n
|
||||
fn position(): (int, int)
|
||||
```
|
||||
|
||||
And destroyed with:
|
||||
At runtime:
|
||||
|
||||
```
|
||||
POP_FRAME
|
||||
stack top → [x, y]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Costs and Limits
|
||||
## 4 Heap Memory
|
||||
|
||||
- frame creation has an explicit cost
|
||||
- maximum stack depth is limited
|
||||
- deep recursion is discouraged
|
||||
All user-defined objects live in the heap.
|
||||
|
||||
PROMETEU favors:
|
||||
### Heap characteristics
|
||||
|
||||
- iteration
|
||||
- explicit state
|
||||
- conscious depth control
|
||||
* Linear slot-based storage.
|
||||
* Objects are fixed-layout blocks.
|
||||
* No raw pointer access.
|
||||
* No inheritance at memory level.
|
||||
|
||||
Heap objects include:
|
||||
|
||||
* user structs/classes
|
||||
* arrays
|
||||
* strings
|
||||
* closures
|
||||
* boxed tuples (optional)
|
||||
|
||||
---
|
||||
|
||||
## 5. Global Space
|
||||
## 5 Handles and Gate Table
|
||||
|
||||
### 5.1 Definition
|
||||
All heap objects are accessed via **handles**.
|
||||
|
||||
The **Global Space** stores:
|
||||
|
||||
- global variables
|
||||
- persistent game state
|
||||
- long-term references
|
||||
|
||||
Globals:
|
||||
|
||||
- survive between frames
|
||||
- occupy memory permanently
|
||||
- count towards total memory usage
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Conscious usage
|
||||
|
||||
PROMETEU encourages:
|
||||
|
||||
- few globals
|
||||
- compact structures
|
||||
- explicit initialization
|
||||
|
||||
Globals are equivalent to **static RAM** in microcontrollers.
|
||||
|
||||
---
|
||||
|
||||
## 6. Heap
|
||||
|
||||
### 6.1 Definition
|
||||
|
||||
The **Heap** is the dynamic memory region of the PROMETEU VM.
|
||||
|
||||
It is used for:
|
||||
|
||||
- objects
|
||||
- arrays
|
||||
- tables
|
||||
- composite structures
|
||||
|
||||
Every allocation on the heap is done explicitly with:
|
||||
A handle is defined as:
|
||||
|
||||
```
|
||||
ALLOC size
|
||||
handle = { index, generation }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Allocation Costs
|
||||
|
||||
Allocating memory:
|
||||
|
||||
- consumes **cycles**
|
||||
- consumes **available heap**
|
||||
- increases pressure on the system
|
||||
|
||||
PROMETEU treats allocation as an **expensive operation by definition**.
|
||||
|
||||
> Allocation is an architectural decision, not a detail.
|
||||
>
|
||||
|
||||
---
|
||||
|
||||
## 7. Heap Limits
|
||||
|
||||
### 7.1 Finite Heap
|
||||
|
||||
The heap:
|
||||
|
||||
- has a defined maximum size
|
||||
- can vary according to the active CAP
|
||||
- never grows dynamically
|
||||
|
||||
Example:
|
||||
The VM maintains a **gate table**:
|
||||
|
||||
```
|
||||
Heap Limit:32KB
|
||||
Heap Used:28KB
|
||||
GateEntry {
|
||||
alive: bool
|
||||
generation: u32
|
||||
base: usize
|
||||
slots: u32
|
||||
type_id: u32
|
||||
}
|
||||
```
|
||||
|
||||
Exceeding the limit:
|
||||
### Handle safety
|
||||
|
||||
- does not crash the game
|
||||
- generates an execution error
|
||||
- appears in the certification report
|
||||
When an object is freed:
|
||||
|
||||
* `alive` becomes false
|
||||
* `generation` is incremented
|
||||
|
||||
When a handle is used:
|
||||
|
||||
* index must exist
|
||||
* generation must match
|
||||
|
||||
Otherwise, the VM traps.
|
||||
|
||||
This prevents:
|
||||
|
||||
* use-after-free
|
||||
* stale references
|
||||
|
||||
---
|
||||
|
||||
### 7.2 Heap and CAP
|
||||
## 6 Object Layout
|
||||
|
||||
During a JAM or evaluation:
|
||||
Heap objects have a simple, fixed layout:
|
||||
|
||||
- heap peak is measured
|
||||
- the value is compared to the CAP limit
|
||||
- non-compliances are recorded
|
||||
```
|
||||
Object {
|
||||
type_id
|
||||
field_0
|
||||
field_1
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The game **continues running**, but the evidence is recorded.
|
||||
Properties:
|
||||
|
||||
* Fields are stored in slot order.
|
||||
* No hidden base classes.
|
||||
* No pointer arithmetic.
|
||||
|
||||
Traits and method dispatch are resolved:
|
||||
|
||||
* statically by the compiler, or
|
||||
* via vtable handles (if dynamic dispatch is used).
|
||||
|
||||
---
|
||||
|
||||
## 8. Garbage Collection (GC)
|
||||
## 7 Closures
|
||||
|
||||
### 8.1 Existence of GC
|
||||
Closures are heap objects.
|
||||
|
||||
PROMETEU may use **simple Garbage Collection**, with the following properties:
|
||||
Layout:
|
||||
|
||||
- non-incremental (v0.1)
|
||||
- explicit cost
|
||||
- observable pauses
|
||||
- documented behavior
|
||||
```
|
||||
Closure {
|
||||
func_id
|
||||
capture_count
|
||||
captures[]
|
||||
}
|
||||
```
|
||||
|
||||
GC **is not invisible**.
|
||||
Captures may be:
|
||||
|
||||
* copied values
|
||||
* handles to heap objects
|
||||
|
||||
Closure environments are part of the GC root set.
|
||||
|
||||
---
|
||||
|
||||
### 8.2 GC Cost
|
||||
## 8 Coroutine Memory
|
||||
|
||||
When GC occurs:
|
||||
Each coroutine owns its own stacks:
|
||||
|
||||
- cycles are consumed
|
||||
- the frame may be impacted
|
||||
- the event is recorded
|
||||
```
|
||||
Coroutine {
|
||||
call_stack
|
||||
operand_stack
|
||||
state
|
||||
}
|
||||
```
|
||||
|
||||
PROMETEU teaches:
|
||||
All coroutine stacks are included in the GC root set.
|
||||
|
||||
> "Creating garbage has a cost."
|
||||
>
|
||||
Coroutines do not share stacks or frames.
|
||||
|
||||
---
|
||||
|
||||
## 9. Memory Best Practices
|
||||
## 9 Garbage Collection
|
||||
|
||||
PROMETEU explicitly encourages:
|
||||
The PVM uses a **mark-sweep collector**.
|
||||
|
||||
- structure reuse
|
||||
- allocation outside the main loop
|
||||
- persistent buffers
|
||||
- pooling manual
|
||||
### GC properties
|
||||
|
||||
And discourages:
|
||||
* Non-moving (no compaction in v1).
|
||||
* Runs only at **safepoints**.
|
||||
* Primary safepoint: `FRAME_SYNC`.
|
||||
|
||||
- per-frame allocation
|
||||
- disposable temporary structures
|
||||
- unplanned growth
|
||||
### GC triggers
|
||||
|
||||
GC may run when:
|
||||
|
||||
* heap usage exceeds threshold
|
||||
* allocation pressure is high
|
||||
|
||||
### Root set
|
||||
|
||||
The collector marks from:
|
||||
|
||||
* operand stack
|
||||
* call stack frames
|
||||
* global variables
|
||||
* coroutine stacks
|
||||
* closure environments
|
||||
* host-held handles
|
||||
|
||||
---
|
||||
|
||||
## 10. Relationship with Microcontrollers
|
||||
## 10 Allocation and Deallocation
|
||||
|
||||
The PROMETEU memory model is intentionally similar to real MCUs:
|
||||
### Allocation
|
||||
|
||||
| MCU | PROMETEU |
|
||||
| --- | --- |
|
||||
| Static RAM | Global Space |
|
||||
| Stack | Call Stack |
|
||||
| Heap | Heap |
|
||||
| Allocation failure | Explicit error |
|
||||
| Fragmentation | Dev's responsibility |
|
||||
Heap allocation:
|
||||
|
||||
This creates a direct transfer of learning.
|
||||
1. VM reserves a slot block.
|
||||
2. A gate entry is created.
|
||||
3. A handle is returned.
|
||||
|
||||
If allocation fails:
|
||||
|
||||
* VM may trigger GC.
|
||||
* If still failing, a trap occurs.
|
||||
|
||||
### Deallocation
|
||||
|
||||
Objects are freed only by the GC.
|
||||
|
||||
When freed:
|
||||
|
||||
* gate is marked dead
|
||||
* generation is incremented
|
||||
* memory becomes available via free list
|
||||
|
||||
---
|
||||
|
||||
## 11. Pedagogical Implications
|
||||
## 11 Host-Owned Memory (Asset Banks)
|
||||
|
||||
This model allows teaching:
|
||||
Asset memory is **not part of the VM heap**.
|
||||
|
||||
- the difference between stack and heap
|
||||
- allocation cost
|
||||
- data lifetime
|
||||
- architectural impact of simple decisions
|
||||
- the relationship between memory and time
|
||||
It is managed by the firmware.
|
||||
|
||||
Everything with **immediate and visible feedback**.
|
||||
Examples:
|
||||
|
||||
* tilebanks
|
||||
* audio sample banks
|
||||
* sprite sheets
|
||||
|
||||
### Properties
|
||||
|
||||
* VM cannot access asset memory directly.
|
||||
* Access occurs only through syscalls.
|
||||
* Asset memory is not scanned by GC.
|
||||
|
||||
---
|
||||
|
||||
## 12. Summary
|
||||
## 12 Save Memory (MEMCARD)
|
||||
|
||||
- PROMETEU has well-defined memory spaces
|
||||
- stack is temporary and cheap
|
||||
- heap is finite and expensive
|
||||
- allocation has an explicit cost
|
||||
- GC is visible and measurable
|
||||
- memory participates in certification
|
||||
Save memory is a host-managed persistent storage area.
|
||||
|
||||
Properties:
|
||||
|
||||
* fixed size
|
||||
* accessed only via syscalls
|
||||
* not part of the VM heap
|
||||
* not scanned by GC
|
||||
|
||||
---
|
||||
|
||||
## 13 Memory Safety Rules
|
||||
|
||||
The VM enforces:
|
||||
|
||||
1. All heap access via handles.
|
||||
2. Generation checks on every handle use.
|
||||
3. Bounds checking on object fields.
|
||||
4. No raw pointer arithmetic.
|
||||
5. Verified stack discipline.
|
||||
|
||||
Any violation results in a trap.
|
||||
|
||||
---
|
||||
|
||||
## 14 Summary
|
||||
|
||||
The PVM memory model is based on:
|
||||
|
||||
* stack-only primitive and tuple values
|
||||
* heap-only user objects
|
||||
* generation-based handles
|
||||
* deterministic GC at frame safepoints
|
||||
* strict separation between VM heap and host memory
|
||||
|
||||
This design ensures:
|
||||
|
||||
* predictable performance
|
||||
* memory safety
|
||||
* simple verification
|
||||
* suitability for real-time game workloads.
|
||||
|
||||
< [Back](chapter-2.md) | [Summary](table-of-contents.md) | [Next](chapter-4.md) >
|
||||
@ -1,289 +1,308 @@
|
||||
< [Back](chapter-8.md) | [Summary](table-of-contents.md) | [Next](chapter-10.md) >
|
||||
|
||||
# ⚡ **Events and Interrupts**
|
||||
# ⚡ **Events and Scheduling**
|
||||
|
||||
## 1. Overview
|
||||
This chapter defines how the Prometeu Virtual Machine (PVM) handles events, frame synchronization, and cooperative concurrency. It replaces the older interrupt-oriented terminology with a simpler and more accurate model based on **frame boundaries**, **event queues**, and **coroutines**.
|
||||
|
||||
PROMETEU clearly distinguishes between **normal execution**, **events**, and **interrupts**.
|
||||
|
||||
Nothing occurs "out of time".
|
||||
|
||||
Nothing interrupts the system without cost.
|
||||
|
||||
Nothing happens without a well-defined point in the execution cycle.
|
||||
|
||||
> Events are signals.
|
||||
> Interrupts are machine decisions.
|
||||
>
|
||||
|
||||
This chapter defines:
|
||||
|
||||
- what PROMETEU considers an event
|
||||
- how interrupts are modeled
|
||||
- when they can occur
|
||||
- how they relate to cycles, CAP, and determinism
|
||||
The goal is to preserve determinism while still allowing responsive and structured game logic.
|
||||
|
||||
---
|
||||
|
||||
## 2. Event Philosophy in PROMETEU
|
||||
## 1 Core Philosophy
|
||||
|
||||
PROMETEU **does not use invisible asynchronous callbacks**.
|
||||
Prometeu does **not use asynchronous callbacks** or preemptive interrupts for user code.
|
||||
|
||||
Every event:
|
||||
All external signals are:
|
||||
|
||||
- is registered
|
||||
- is delivered at a predictable moment
|
||||
- is handled within the main loop
|
||||
* queued by the firmware
|
||||
* delivered at deterministic points
|
||||
* processed inside the main execution loop
|
||||
|
||||
This model avoids:
|
||||
Nothing executes “out of time”.
|
||||
Nothing interrupts the program in the middle of an instruction.
|
||||
Nothing occurs without a known cost.
|
||||
|
||||
- implicit concurrency
|
||||
- hidden race conditions
|
||||
- non-deterministic side effects
|
||||
This guarantees:
|
||||
|
||||
PROMETEU favors:
|
||||
|
||||
> explicit control over reactivity.
|
||||
>
|
||||
* deterministic behavior
|
||||
* reproducible runs
|
||||
* stable frame timing
|
||||
|
||||
---
|
||||
|
||||
## 3. Events
|
||||
## 2 Events
|
||||
|
||||
### 3.1 Definition
|
||||
### 2.1 Definition
|
||||
|
||||
An **event** in PROMETEU is a **logical signal** generated by the system or the program, indicating that something has occurred.
|
||||
|
||||
Examples of events:
|
||||
|
||||
- end of frame
|
||||
- timer expired
|
||||
- system state change
|
||||
- execution error
|
||||
|
||||
Events **do not execute code automatically**.
|
||||
|
||||
They only **inform**.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Event Queue
|
||||
|
||||
PROMETEU maintains an **event queue**:
|
||||
|
||||
- events are queued
|
||||
- the queue is processed in order
|
||||
- processing occurs at defined points in the frame
|
||||
An **event** is a logical signal generated by the system or by internal runtime mechanisms.
|
||||
|
||||
Events:
|
||||
|
||||
- do not interrupt execution arbitrarily
|
||||
- do not execute outside the loop
|
||||
* represent something that has occurred
|
||||
* do not execute code automatically
|
||||
* are processed explicitly during the frame
|
||||
|
||||
Examples:
|
||||
|
||||
* end of frame
|
||||
* timer expiration
|
||||
* asset load completion
|
||||
* system state change
|
||||
* execution error
|
||||
|
||||
---
|
||||
|
||||
## 4. Interrupts
|
||||
### 2.2 Event Queue
|
||||
|
||||
### 4.1 Definition
|
||||
The firmware maintains an **event queue**.
|
||||
|
||||
An **interrupt** is a special event, treated by the system as **priority**, which can:
|
||||
Properties:
|
||||
|
||||
- change the normal flow of execution
|
||||
- execute specific system code
|
||||
- impact cycles and budget
|
||||
* events are queued in order
|
||||
* events are processed at frame boundaries
|
||||
* event processing is deterministic
|
||||
|
||||
Interrupts are **rare and explicit**.
|
||||
Events never:
|
||||
|
||||
* execute user code automatically
|
||||
* interrupt instructions
|
||||
* run outside the main loop
|
||||
|
||||
---
|
||||
|
||||
### 4.2 What is NOT an interrupt
|
||||
## 3 Frame Boundary (Sync Phase)
|
||||
|
||||
In PROMETEU, the following are **not interrupts**:
|
||||
The primary synchronization point in Prometeu is the **frame boundary**, reached by the `FRAME_SYNC` instruction.
|
||||
|
||||
- button input
|
||||
- collisions
|
||||
- common timers
|
||||
- game logic
|
||||
At this point:
|
||||
|
||||
These are treated as **state or normal events**.
|
||||
1. Input state is sampled.
|
||||
2. Events are delivered.
|
||||
3. Coroutine scheduling occurs.
|
||||
4. Optional garbage collection may run.
|
||||
5. Control returns to the firmware.
|
||||
|
||||
This replaces the older notion of a "VBlank interrupt".
|
||||
|
||||
The frame boundary:
|
||||
|
||||
* has a fixed, measurable cost
|
||||
* does not execute arbitrary user code
|
||||
* is fully deterministic
|
||||
|
||||
---
|
||||
|
||||
## 5. Types of Interrupts in PROMETEU
|
||||
## 4 System Events vs System Faults
|
||||
|
||||
PROMETEU defines a small and well-controlled set of interrupts.
|
||||
Prometeu distinguishes between normal events and system faults.
|
||||
|
||||
### 5.1 Frame Interrupt (Conceptual VBlank)
|
||||
### 4.1 Normal events
|
||||
|
||||
The end of each frame generates a **logical synchronization interrupt**, responsible for:
|
||||
Examples:
|
||||
|
||||
- framebuffer swap
|
||||
- audio commit
|
||||
- state synchronization
|
||||
* timer expired
|
||||
* asset loaded
|
||||
* frame boundary
|
||||
|
||||
This interrupt:
|
||||
These are delivered through the event queue.
|
||||
|
||||
- occurs at SYNC
|
||||
- has a fixed cost
|
||||
- does not execute user code
|
||||
|
||||
---
|
||||
|
||||
### 5.2 System Interrupt
|
||||
### 4.2 System faults
|
||||
|
||||
Generated by exceptional conditions:
|
||||
|
||||
- fatal VM error
|
||||
- memory violation
|
||||
- invalid instruction
|
||||
* invalid instruction
|
||||
* memory violation
|
||||
* handle misuse
|
||||
* verifier failure
|
||||
|
||||
Result:
|
||||
|
||||
- execution is halted
|
||||
- VM state is preserved
|
||||
- detailed report is generated
|
||||
* VM execution stops
|
||||
* state is preserved
|
||||
* a diagnostic report is generated
|
||||
|
||||
System faults are not recoverable events.
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Timed Interrupts (Timers)
|
||||
## 5 Timers
|
||||
|
||||
PROMETEU can offer **system timers**, modeled as:
|
||||
Timers are modeled as **frame-based counters**.
|
||||
|
||||
- counters based on frames
|
||||
- signals generated upon reaching zero
|
||||
Properties:
|
||||
|
||||
* measured in frames, not real time
|
||||
* deterministic across runs
|
||||
* generate events when they expire
|
||||
|
||||
Timers:
|
||||
|
||||
- do not trigger code automatically
|
||||
- generate queryable events
|
||||
* do not execute code automatically
|
||||
* produce queryable or queued events
|
||||
|
||||
Conceptual example:
|
||||
Example:
|
||||
|
||||
```
|
||||
if (timer.expired(T1)) {
|
||||
// handle event
|
||||
if timer.expired(t1) {
|
||||
// handle event
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Relationship between Events, Interrupts, and the Loop
|
||||
## 6 Coroutines and Cooperative Scheduling
|
||||
|
||||
The complete flow can be represented as follows:
|
||||
Prometeu provides **coroutines** as the only form of concurrency.
|
||||
|
||||
```
|
||||
FRAME N
|
||||
──────────────
|
||||
SAMPLEINPUT
|
||||
PROCESS EVENTS
|
||||
UPDATE
|
||||
DRAW
|
||||
AUDIO
|
||||
INTERRUPT: VBLANK
|
||||
SYNC
|
||||
──────────────
|
||||
```
|
||||
Coroutines are:
|
||||
|
||||
Important:
|
||||
* cooperative
|
||||
* deterministic
|
||||
* scheduled only at safe points
|
||||
|
||||
- events are processed before main logic
|
||||
- interrupts occur only at safe points
|
||||
- no interrupt occurs "in the middle" of an instruction
|
||||
There is:
|
||||
|
||||
* no preemption
|
||||
* no parallel execution
|
||||
* no hidden threads
|
||||
|
||||
### 6.1 Coroutine lifecycle
|
||||
|
||||
Each coroutine can be in one of the following states:
|
||||
|
||||
* `Ready`
|
||||
* `Running`
|
||||
* `Sleeping`
|
||||
* `Finished`
|
||||
|
||||
### 6.2 Scheduling
|
||||
|
||||
At each frame boundary:
|
||||
|
||||
1. The scheduler selects the next coroutine.
|
||||
2. Coroutines run in a deterministic order.
|
||||
3. Each coroutine executes within the frame budget.
|
||||
|
||||
The default scheduling policy is:
|
||||
|
||||
* round-robin
|
||||
* deterministic
|
||||
|
||||
---
|
||||
|
||||
## 7. Costs and Budget
|
||||
### 6.3 Coroutine operations
|
||||
|
||||
Events and interrupts:
|
||||
Typical coroutine instructions:
|
||||
|
||||
- consume cycles
|
||||
- participate in the CAP
|
||||
- appear in certification
|
||||
| Operation | Description |
|
||||
| --------- | ----------------------------- |
|
||||
| `spawn` | Create a coroutine |
|
||||
| `yield` | Voluntarily suspend execution |
|
||||
| `sleep` | Suspend for N frames |
|
||||
|
||||
Example report:
|
||||
`yield` and `sleep` only take effect at safe points.
|
||||
|
||||
---
|
||||
|
||||
## 7 Relationship Between Events, Coroutines, and the Frame Loop
|
||||
|
||||
The high-level frame structure is:
|
||||
|
||||
```
|
||||
FRAME N
|
||||
------------------------
|
||||
Sample Input
|
||||
Deliver Events
|
||||
Schedule Coroutines
|
||||
Run VM until:
|
||||
- budget exhausted, or
|
||||
- FRAME_SYNC reached
|
||||
Sync Phase
|
||||
------------------------
|
||||
```
|
||||
|
||||
Important properties:
|
||||
|
||||
* events are processed at known points
|
||||
* coroutine scheduling is deterministic
|
||||
* no execution occurs outside the frame loop
|
||||
|
||||
---
|
||||
|
||||
## 8 Costs and Budget
|
||||
|
||||
All event processing and scheduling:
|
||||
|
||||
* consumes cycles
|
||||
* contributes to the CAP (certification and analysis profile)
|
||||
* appears in profiling reports
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
Frame 18231:
|
||||
Event processing:120cycles
|
||||
VBlank interrupt:80cycles
|
||||
Event processing: 120 cycles
|
||||
Coroutine scheduling: 40 cycles
|
||||
Frame sync: 80 cycles
|
||||
```
|
||||
|
||||
Nothing is free.
|
||||
|
||||
---
|
||||
|
||||
## 8. Determinism and Reproducibility
|
||||
## 9 Determinism and Reproducibility
|
||||
|
||||
PROMETEU guarantees:
|
||||
Prometeu guarantees:
|
||||
|
||||
- same sequence of events → same behavior
|
||||
- interrupts always at the same point in the frame
|
||||
- timers based on frame count, not real time
|
||||
* same sequence of inputs and events → same behavior
|
||||
* frame-based timers
|
||||
* deterministic coroutine scheduling
|
||||
|
||||
This allows:
|
||||
|
||||
- reliable replays
|
||||
- precise debugging
|
||||
- fair certification
|
||||
* reliable replays
|
||||
* precise debugging
|
||||
* fair certification
|
||||
|
||||
---
|
||||
|
||||
## 9. Best Practices
|
||||
## 10 Best Practices
|
||||
|
||||
PROMETEU encourages:
|
||||
Prometeu encourages:
|
||||
|
||||
- treating events as data
|
||||
- querying events explicitly
|
||||
- avoiding heavy logic in handlers
|
||||
- using timers instead of excessive polling
|
||||
* treating events as data
|
||||
* querying events explicitly
|
||||
* structuring logic around frame steps
|
||||
* using coroutines for asynchronous flows
|
||||
|
||||
PROMETEU discourages:
|
||||
Prometeu discourages:
|
||||
|
||||
- simulating asynchronous callbacks
|
||||
- depending on implicit order
|
||||
- using events as a "shortcut" for complex logic
|
||||
* simulating asynchronous callbacks
|
||||
* relying on hidden timing
|
||||
* using events as implicit control flow
|
||||
|
||||
---
|
||||
|
||||
## 10. Relationship with Microcontrollers
|
||||
## 11 Conceptual Comparison
|
||||
|
||||
The model reflects real MCUs:
|
||||
| Traditional System | Prometeu |
|
||||
| ------------------ | ----------------------- |
|
||||
| Hardware interrupt | Frame boundary event |
|
||||
| ISR | System sync phase |
|
||||
| Main loop | VM frame loop |
|
||||
| Timer interrupt | Frame-based timer event |
|
||||
| Threads | Coroutines |
|
||||
|
||||
| MCU | PROMETEU |
|
||||
| --- | --- |
|
||||
| ISR | Explicit interrupt |
|
||||
| Main Loop | PROMETEU Loop |
|
||||
| Flags | Events |
|
||||
| Timers | Per-frame timers |
|
||||
|
||||
But without:
|
||||
|
||||
- real concurrency
|
||||
- unpredictable interrupts
|
||||
|
||||
PROMETEU **teaches the concept**, not the chaos.
|
||||
Prometeu teaches reactive system concepts without the unpredictability of real interrupts.
|
||||
|
||||
---
|
||||
|
||||
## 11. Pedagogical Implications
|
||||
## 12 Summary
|
||||
|
||||
This model allows teaching:
|
||||
|
||||
- the difference between event and interrupt
|
||||
- safe synchronization
|
||||
- flow control in reactive systems
|
||||
- the impact of temporal decisions
|
||||
|
||||
Everything with **order, clarity, and measurement**.
|
||||
|
||||
---
|
||||
|
||||
## 12. Summary
|
||||
|
||||
- events inform, they do not execute
|
||||
- interrupts are rare and controlled
|
||||
- no execution occurs outside the loop
|
||||
- costs are explicit
|
||||
- behavior is deterministic
|
||||
* Events inform; they do not execute code.
|
||||
* The frame boundary is the only global synchronization point.
|
||||
* System faults stop execution.
|
||||
* Coroutines provide cooperative concurrency.
|
||||
* All behavior is deterministic and measurable.
|
||||
|
||||
< [Back](chapter-8.md) | [Summary](table-of-contents.md) | [Next](chapter-10.md) >
|
||||
@ -15,6 +15,7 @@
|
||||
- [Chapter 13: Cartridge](chapter-13.md)
|
||||
- [Chapter 14: Boot Profiles](chapter-14.md)
|
||||
- [Chapter 15: Asset Management](chapter-15.md)
|
||||
- [Chapter 16: Host ABI and Syscalls](chapter-16.md)
|
||||
|
||||
---
|
||||
[Back to README](../README.md)
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "//",
|
||||
"blockComment": ["/*", "*/"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["[[", "]]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "[[", "close": "]]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"" }
|
||||
]
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.activate = activate;
|
||||
exports.deactivate = deactivate;
|
||||
const vscode = require("vscode");
|
||||
const node_1 = require("vscode-languageclient/node");
|
||||
let client;
|
||||
function activate(context) {
|
||||
const cfg = vscode.workspace.getConfiguration("prometeuPbs");
|
||||
const serverPath = cfg.get("serverPath");
|
||||
if (!serverPath) {
|
||||
vscode.window.showErrorMessage("Prometeu PBS: configure 'prometeuPbs.serverPath' com o caminho do bin prometeu-lsp.");
|
||||
return;
|
||||
}
|
||||
const serverOptions = {
|
||||
command: serverPath,
|
||||
args: []
|
||||
};
|
||||
const clientOptions = {
|
||||
documentSelector: [{ scheme: "file", language: "pbs" }]
|
||||
};
|
||||
client = new node_1.LanguageClient("prometeuPbsLsp", "Prometeu PBS LSP", serverOptions, clientOptions);
|
||||
// ✅ O client é “parável” no deactivate, não o start() Promise
|
||||
context.subscriptions.push({
|
||||
dispose: () => {
|
||||
void client?.stop();
|
||||
}
|
||||
});
|
||||
void client.start();
|
||||
}
|
||||
function deactivate() {
|
||||
return client?.stop();
|
||||
}
|
||||
//# sourceMappingURL=extension.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":";;AAKA,4BA8BC;AAED,gCAEC;AAvCD,iCAAiC;AACjC,qDAAkG;AAElG,IAAI,MAAkC,CAAC;AAEvC,SAAgB,QAAQ,CAAC,OAAgC;IACrD,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC;IAC7D,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,CAAS,YAAY,CAAC,CAAC;IAEjD,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAC1B,qFAAqF,CACxF,CAAC;QACF,OAAO;IACX,CAAC;IAED,MAAM,aAAa,GAAkB;QACjC,OAAO,EAAE,UAAU;QACnB,IAAI,EAAE,EAAE;KACX,CAAC;IAEF,MAAM,aAAa,GAA0B;QACzC,gBAAgB,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;KAC1D,CAAC;IAEF,MAAM,GAAG,IAAI,qBAAc,CAAC,gBAAgB,EAAE,kBAAkB,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC;IAEhG,8DAA8D;IAC9D,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC;QACvB,OAAO,EAAE,GAAG,EAAE;YACV,KAAK,MAAM,EAAE,IAAI,EAAE,CAAC;QACxB,CAAC;KACJ,CAAC,CAAC;IAEH,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC;AAED,SAAgB,UAAU;IACtB,OAAO,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC"}
|
||||
7378
prometeu-vscode/package-lock.json
generated
7378
prometeu-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "prometeu-pbs",
|
||||
"displayName": "Prometeu PBS",
|
||||
"version": "0.0.1",
|
||||
"publisher": "local",
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
},
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "pbs",
|
||||
"aliases": [
|
||||
"PBS",
|
||||
"Prometeu Base Script"
|
||||
],
|
||||
"extensions": [
|
||||
".pbs"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "Prometeu PBS",
|
||||
"properties": {
|
||||
"prometeuPbs.serverPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/target/debug/prometeu-lsp --stdio"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"generator-code": "^1.11.17",
|
||||
"typescript": "^5.0.0",
|
||||
"yo": "^6.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "tsc -p .",
|
||||
"watch": "tsc -watch -p ."
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"prometeuPbs.serverPath": "/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/target/debug/prometeu-lsp"
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import * as vscode from "vscode";
|
||||
import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient/node";
|
||||
|
||||
let client: LanguageClient | undefined;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const cfg = vscode.workspace.getConfiguration("prometeuPbs");
|
||||
const serverPath = cfg.get<string>("serverPath");
|
||||
|
||||
if (!serverPath) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Prometeu PBS: configure 'prometeuPbs.serverPath' com o caminho do bin prometeu-lsp."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const serverOptions: ServerOptions = {
|
||||
command: serverPath,
|
||||
args: []
|
||||
};
|
||||
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector: [{ scheme: "file", language: "pbs" }]
|
||||
};
|
||||
|
||||
client = new LanguageClient("prometeuPbsLsp", "Prometeu PBS LSP", serverOptions, clientOptions);
|
||||
|
||||
// ✅ O client é “parável” no deactivate, não o start() Promise
|
||||
context.subscriptions.push({
|
||||
dispose: () => {
|
||||
void client?.stop();
|
||||
}
|
||||
});
|
||||
|
||||
void client.start();
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> | undefined {
|
||||
return client?.stop();
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2020",
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"lib": ["ES2020"],
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user