From b609b2e94b7da873071eb9ea8fca49d1723875ef Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:07:12 -0700 Subject: [PATCH] device component with event to update state Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- ic10emu/build.rs | 38 ++-- ic10emu/src/grammar.rs | 10 +- ic10emu/src/interpreter.rs | 21 +- ic10emu/src/lib.rs | 33 ++- ic10emu_wasm/src/lib.rs | 50 ++--- ic10emu_wasm/src/types.ts | 130 +++++++++--- www/src/js/app/app.ts | 27 +-- www/src/js/components/base.ts | 8 + www/src/js/editor/ace.ts | 1 + www/src/js/editor/index.ts | 58 +++--- .../{shortcuts-ui.ts => shortcuts_ui.ts} | 0 www/src/js/editor/styles.ts | 5 + www/src/js/session.ts | 54 ++++- www/src/js/utils.ts | 45 ++++- www/src/js/virtual_machine/base_device.ts | 78 +++++++ www/src/js/virtual_machine/controls.ts | 190 ++++++++++++++++++ www/src/js/virtual_machine/index.ts | 44 ++-- www/src/js/virtual_machine/ui.ts | 17 ++ 18 files changed, 656 insertions(+), 153 deletions(-) rename www/src/js/editor/{shortcuts-ui.ts => shortcuts_ui.ts} (100%) create mode 100644 www/src/js/virtual_machine/base_device.ts create mode 100644 www/src/js/virtual_machine/controls.ts create mode 100644 www/src/js/virtual_machine/ui.ts diff --git a/ic10emu/build.rs b/ic10emu/build.rs index cdfded7..a04e480 100644 --- a/ic10emu/build.rs +++ b/ic10emu/build.rs @@ -50,7 +50,7 @@ fn write_repr_enum( ) .unwrap(); for (name, variant) in variants { - let variant_name = name.to_case(Case::Pascal); + let variant_name = name.replace('.', "").to_case(Case::Pascal); let mut serialize = vec![name.clone()]; serialize.extend(variant.aliases.iter().cloned()); let serialize_str = serialize @@ -186,31 +186,31 @@ fn write_enums() { let output_file = File::create(dest_path).unwrap(); let mut writer = BufWriter::new(&output_file); - let mut enums_lookup_map_builder = ::phf_codegen::Map::new(); - let mut check_set = std::collections::HashSet::new(); + let mut enums_map: HashMap> = HashMap::new(); let e_infile = Path::new("data/enums.txt"); let e_contents = fs::read_to_string(e_infile).unwrap(); for line in e_contents.lines().filter(|l| !l.trim().is_empty()) { - let (name, val_str) = line.split_once(' ').unwrap(); + let mut it = line.splitn(3, ' '); + let name = it.next().unwrap(); + let val_str = it.next().unwrap(); + let val: Option = val_str.parse().ok(); + let docs = it.next(); + let deprecated = docs + .map(|docs| docs.trim().to_uppercase() == "DEPRECATED") + .unwrap_or(false); - let val: Option = val_str.parse().ok(); - - if !check_set.contains(name) { - check_set.insert(name); - } - - if let Some(v) = val { - enums_lookup_map_builder.entry(name, &format!("{}u8", v)); - } + enums_map.insert( + name.to_string(), + EnumVariant { + aliases: Vec::new(), + value: val, + deprecated, + }, + ); } - writeln!( - &mut writer, - "pub(crate) const ENUM_LOOKUP: phf::Map<&'static str, u8> = {};", - enums_lookup_map_builder.build() - ) - .unwrap(); + write_repr_enum(&mut writer, "LogicEnums", &enums_map, true); println!("cargo:rerun-if-changed=data/enums.txt"); } diff --git a/ic10emu/src/grammar.rs b/ic10emu/src/grammar.rs index b84052f..d66c9ed 100644 --- a/ic10emu/src/grammar.rs +++ b/ic10emu/src/grammar.rs @@ -760,8 +760,10 @@ impl FromStr for Operand { } } else if let Some(val) = CONSTANTS_LOOKUP.get(s) { Ok(Operand::Number(Number::Constant(*val))) - } else if let Some(val) = ENUM_LOOKUP.get(s) { - Ok(Operand::Number(Number::Enum(*val as f64))) + } else if let Ok(val) = LogicEnums::from_str(s) { + Ok(Operand::Number(Number::Enum( + val.get_str("value").unwrap().parse().unwrap(), + ))) } else if let Ok(lt) = LogicType::from_str(s) { Ok(Operand::LogicType(lt)) } else if let Ok(slt) = SlotLogicType::from_str(s) { @@ -1127,9 +1129,7 @@ mod tests { indirection: 0, target: 2, }), - Operand::Identifier(Identifier { - name: "LogicType.Temperature".to_owned() - }), + Operand::Number(Number::Enum(6.0)), ], },),), comment: None, diff --git a/ic10emu/src/interpreter.rs b/ic10emu/src/interpreter.rs index 30dd797..77677c3 100644 --- a/ic10emu/src/interpreter.rs +++ b/ic10emu/src/interpreter.rs @@ -58,7 +58,7 @@ pub enum ICError { index: u32, desired: String, }, - #[error("Unknown identifier '{0}")] + #[error("Unknown identifier {0}")] UnknownIdentifier(String), #[error("Device Not Set")] DeviceNotSet, @@ -2030,9 +2030,9 @@ impl IC { let RegisterSpec { indirection, target, - } = reg.as_register(this, inst, 1)?; - let (Some(device_id), _connection) = dev_id.as_device(this, inst, 2)? else { + let (Some(device_id), _connection) = dev_id.as_device(this, inst, 2)? + else { return Err(DeviceNotSet); }; let device = vm.get_device_same_network(this.device, device_id); @@ -2057,9 +2057,9 @@ impl IC { let RegisterSpec { indirection, target, - } = reg.as_register(this, inst, 1)?; - let (Some(device_id), _connection) = dev_id.as_device(this, inst, 2)? else { + let (Some(device_id), _connection) = dev_id.as_device(this, inst, 2)? + else { return Err(DeviceNotSet); }; let device = vm.get_device_same_network(this.device, device_id); @@ -2080,9 +2080,9 @@ impl IC { oprs => Err(ICError::mismatch_operands(oprs.len(), 3)), }, Put => match &operands[..] { - [dev_id, addr, val] => { - let (Some(device_id), _connection) = dev_id.as_device(this, inst, 1)? else { + let (Some(device_id), _connection) = dev_id.as_device(this, inst, 1)? + else { return Err(DeviceNotSet); }; let device = vm.get_device_same_network(this.device, device_id); @@ -2093,6 +2093,7 @@ impl IC { let val = val.as_value(this, inst, 3)?; let mut ic = vm.ics.get(ic_id).unwrap().borrow_mut(); ic.poke(addr, val)?; + vm.set_modified(device_id); Ok(()) } None => Err(DeviceHasNoIC), @@ -2116,6 +2117,7 @@ impl IC { let val = val.as_value(this, inst, 3)?; let mut ic = vm.ics.get(ic_id).unwrap().borrow_mut(); ic.poke(addr, val)?; + vm.set_modified(device_id as u16); Ok(()) } None => Err(DeviceHasNoIC), @@ -2150,6 +2152,7 @@ impl IC { Some(device) => { let val = val.as_value(this, inst, 1)?; device.borrow_mut().set_field(lt, val)?; + vm.set_modified(device_id); Ok(()) } None => Err(UnknownDeviceID(device_id as f64)), @@ -2169,6 +2172,7 @@ impl IC { let lt = LogicType::try_from(lt.as_value(this, inst, 2)?)?; let val = val.as_value(this, inst, 3)?; device.borrow_mut().set_field(lt, val)?; + vm.set_modified(device_id as u16); Ok(()) } None => Err(UnknownDeviceID(device_id)), @@ -2188,6 +2192,7 @@ impl IC { let lt = SlotLogicType::try_from(lt.as_value(this, inst, 3)?)?; let val = val.as_value(this, inst, 4)?; device.borrow_mut().set_slot_field(index, lt, val)?; + vm.set_modified(device_id); Ok(()) } None => Err(UnknownDeviceID(device_id as f64)), @@ -2410,8 +2415,8 @@ impl IC { let result = process_op(self); if result.is_ok() || advance_ip_on_err { self.ic += 1; + self.ip = next_ip; } - self.ip = next_ip; result } } diff --git a/ic10emu/src/lib.rs b/ic10emu/src/lib.rs index a41db7a..e45ecec 100644 --- a/ic10emu/src/lib.rs +++ b/ic10emu/src/lib.rs @@ -200,6 +200,9 @@ pub struct VM { id_gen: IdSequenceGenerator, network_id_gen: IdSequenceGenerator, random: Rc>, + + /// list of device id's touched on the last operation + operation_modified: RefCell>, } impl Default for Network { @@ -410,6 +413,7 @@ impl VM { id_gen, network_id_gen, random: Rc::new(RefCell::new(crate::rand_mscorlib::Random::new())), + operation_modified: RefCell::new(Vec::new()), }; let _ = vm.add_ic(None); vm @@ -555,9 +559,16 @@ impl VM { Ok(true) } + /// returns a list of device ids modified in the last operations + pub fn last_operation_modified(&self) -> Vec { + self.operation_modified.borrow().clone() + } + pub fn step_ic(&self, id: u16, advance_ip_on_err: bool) -> Result { + self.operation_modified.borrow_mut().clear(); let device = self.devices.get(&id).ok_or(VMError::UnknownId(id))?.clone(); let ic_id = *device.borrow().ic.as_ref().ok_or(VMError::NoIC(id))?; + self.set_modified(id); let ic = self .ics .get(&ic_id) @@ -570,6 +581,7 @@ impl VM { /// returns true if executed 128 lines, false if returned early. pub fn run_ic(&self, id: u16, ignore_errors: bool) -> Result { + self.operation_modified.borrow_mut().clear(); let device = self.devices.get(&id).ok_or(VMError::UnknownId(id))?.clone(); let ic_id = *device.borrow().ic.as_ref().ok_or(VMError::NoIC(id))?; let ic = self @@ -578,6 +590,7 @@ impl VM { .ok_or(VMError::UnknownIcId(ic_id))? .clone(); ic.borrow_mut().ic = 0; + self.set_modified(id); for _i in 0..128 { if let Err(err) = ic.borrow_mut().step(self, ignore_errors) { if !ignore_errors { @@ -594,6 +607,10 @@ impl VM { Ok(true) } + pub fn set_modified(&self, id: u16) { + self.operation_modified.borrow_mut().push(id); + } + pub fn reset_ic(&self, id: u16) -> Result { let device = self.devices.get(&id).ok_or(VMError::UnknownId(id))?.clone(); let ic_id = *device.borrow().ic.as_ref().ok_or(VMError::NoIC(id))?; @@ -666,6 +683,7 @@ impl VM { } fn add_device_to_network(&self, id: u16, network_id: u16) -> Result { + self.set_modified(id); if !self.devices.contains_key(&id) { return Err(VMError::UnknownId(id)); }; @@ -685,7 +703,10 @@ impl VM { val: f64, ) -> Result<(), ICError> { self.batch_device(source, prefab, None) - .map(|device| device.borrow_mut().set_field(typ, val)) + .map(|device| { + self.set_modified(device.borrow().id); + device.borrow_mut().set_field(typ, val) + }) .try_collect() } @@ -698,7 +719,10 @@ impl VM { val: f64, ) -> Result<(), ICError> { self.batch_device(source, prefab, None) - .map(|device| device.borrow_mut().set_slot_field(index, typ, val)) + .map(|device| { + self.set_modified(device.borrow().id); + device.borrow_mut().set_slot_field(index, typ, val) + }) .try_collect() } @@ -711,7 +735,10 @@ impl VM { val: f64, ) -> Result<(), ICError> { self.batch_device(source, prefab, Some(name)) - .map(|device| device.borrow_mut().set_field(typ, val)) + .map(|device| { + self.set_modified(device.borrow().id); + device.borrow_mut().set_field(typ, val) + }) .try_collect() } diff --git a/ic10emu_wasm/src/lib.rs b/ic10emu_wasm/src/lib.rs index ac86864..3e30511 100644 --- a/ic10emu_wasm/src/lib.rs +++ b/ic10emu_wasm/src/lib.rs @@ -79,14 +79,13 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| ic.as_ref().borrow().ip) }) - .flatten() } #[wasm_bindgen(getter, js_name = "instructionCount")] @@ -95,14 +94,13 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| ic.as_ref().borrow().ic) }) - .flatten() } #[wasm_bindgen(getter, js_name = "stack")] @@ -111,14 +109,13 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| Stack(ic.as_ref().borrow().stack)) }) - .flatten() } #[wasm_bindgen(getter, js_name = "registers")] @@ -127,14 +124,13 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| Registers(ic.as_ref().borrow().registers)) }) - .flatten() } #[wasm_bindgen(getter, js_name = "aliases", skip_typescript)] @@ -145,14 +141,13 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| ic.as_ref().borrow().aliases.clone()) - }) - .flatten(), + }), ) .unwrap() } @@ -165,14 +160,13 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| ic.as_ref().borrow().defines.clone()) - }) - .flatten(), + }), ) .unwrap() } @@ -185,14 +179,13 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| ic.as_ref().borrow().pins) - }) - .flatten(), + }), ) .unwrap() } @@ -203,18 +196,17 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| ic.borrow().state.clone()) }) - .flatten() .map(|state| state.to_string()) } - #[wasm_bindgen(getter, js_name = "program")] + #[wasm_bindgen(getter, js_name = "program", skip_typescript)] pub fn ic_program(&self) -> JsValue { serde_wasm_bindgen::to_value( &self @@ -222,14 +214,13 @@ impl DeviceRef { .borrow() .ic .as_ref() - .map(|ic| { + .and_then(|ic| { self.vm .borrow() .ics .get(ic) .map(|ic| ic.borrow().program.clone()) - }) - .flatten(), + }), ) .unwrap() } @@ -306,6 +297,7 @@ impl DeviceRef { pub struct VM { vm: Rc>, } + #[wasm_bindgen] impl VM { #[wasm_bindgen(constructor)] @@ -372,6 +364,18 @@ impl VM { pub fn ics(&self) -> Vec { self.vm.borrow().ics.keys().copied().collect_vec() } + + #[wasm_bindgen(getter, js_name = "lastOperationModified")] + pub fn last_operation_modified(&self) -> Vec { + self.vm.borrow().last_operation_modified() + } + +} + +impl Default for VM { + fn default() -> Self { + Self::new() + } } #[wasm_bindgen] diff --git a/ic10emu_wasm/src/types.ts b/ic10emu_wasm/src/types.ts index 6bb32a8..af96e6b 100644 --- a/ic10emu_wasm/src/types.ts +++ b/ic10emu_wasm/src/types.ts @@ -1,36 +1,120 @@ -type FieldType = 'Read' | 'Write' | 'ReadWrite'; +export type FieldType = "Read" | "Write" | "ReadWrite"; -interface LogicField { - field_type: FieldType, - value: number, +export interface LogicField { + field_type: FieldType; + value: number; } -type Fields = Map; +export type Fields = Map; -type SlotType = 'AccessCard' | 'Appliance' | 'Back' | 'Battery' | 'Blocked' | 'Bottle' | 'Cartridge' | 'Circuitboard' | 'CreditCard' | 'DataDisk' | 'DrillHead' | 'Egg' | 'Entity' | 'Flare' | 'GasCanister' | 'GasFilter' | 'Helmet' | 'Ingot' | 'LiquidBottle' | 'LiquidCanister' | 'Magazine' | 'Ore' | 'Organ' | 'Plant' | 'ProgramableChip' | 'ScanningHead' | 'SensorProcessingUnit' | 'SoundCartridge' | 'Suit' | 'Tool' | 'Torpedo' | 'None'; - -interface Slot { - typ: SlotType, - fields: Fields, +export type SlotType = + | "AccessCard" + | "Appliance" + | "Back" + | "Battery" + | "Blocked" + | "Bottle" + | "Cartridge" + | "Circuitboard" + | "CreditCard" + | "DataDisk" + | "DrillHead" + | "Egg" + | "Entity" + | "Flare" + | "GasCanister" + | "GasFilter" + | "Helmet" + | "Ingot" + | "LiquidBottle" + | "LiquidCanister" + | "Magazine" + | "Ore" + | "Organ" + | "Plant" + | "ProgramableChip" + | "ScanningHead" + | "SensorProcessingUnit" + | "SoundCartridge" + | "Suit" + | "Tool" + | "Torpedo" + | "None"; + +export interface Slot { + typ: SlotType; + fields: Fields; } -type Reagents = Map>; +export type Reagents = Map>; -type Connection = { CableNetwork: number } | 'Other' ; +export type Connection = { CableNetwork: number } | "Other"; -type Alias = { RegisterSpec: {indirection: number, target: number} } | { DeviceSpec: { device: "Db" | { Numbered: number } | { Indirect: { indirection: number, target: number } } }, connection: number | undefined }; +export type Alias = + | { RegisterSpec: { indirection: number; target: number } } + | { + DeviceSpec: { + device: + | "Db" + | { Numbered: number } + | { Indirect: { indirection: number; target: number } }; + }; + connection: number | undefined; + }; -type Aliases = Map; +export type Aliases = Map; -type Defines = Map; +export type Defines = Map; -type Pins = (number | undefined)[] +export type Pins = (number | undefined)[]; + +export type Operand = + | { RegisterSpec: { indirection: number; target: number } } + | { + DeviceSpec: { + device: + | "Db" + | { Numbered: number } + | { Indirect: { indirection: number; target: number } }; + }; + connection: number | undefined; + } + | { + Number: + | { Float: number } + | { Binary: number } + | { Hexadecimal: number } + | { Constant: number } + | { String: string } + | { Enum: number }; + } + | { LogicType: string } + | { SlotLogicType: string } + | { BatchMode: string } + | { ReagentMode: string } + | { Identifier: { name: string } }; + +export interface Instruction { + instruction: string; + operands: Operand[]; +} + +export type ICError = { + ParseError: { line: number; start: number; end: number; msg: string }; +}; + +export interface Program { + instructions: Instruction[]; + errors: ICError[]; + labels: Map; +} export interface DeviceRef { - readonly fields: Fields; - readonly slots: Slot[]; - readonly reagents: Reagents; - readonly connections: Connection[]; - readonly aliases?: Aliases | undefined; - readonly defines?: Defines | undefined; - readonly pins?: Pins; + readonly fields: Fields; + readonly slots: Slot[]; + readonly reagents: Reagents; + readonly connections: Connection[]; + readonly aliases?: Aliases | undefined; + readonly defines?: Defines | undefined; + readonly pins?: Pins; + readonly program?: Program; } diff --git a/www/src/js/app/app.ts b/www/src/js/app/app.ts index 1a59c3e..85dd0d3 100644 --- a/www/src/js/app/app.ts +++ b/www/src/js/app/app.ts @@ -5,10 +5,10 @@ import "./nav"; import "./share"; import { ShareSessionDialog } from "./share"; -import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js'; +import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js"; // Set the base path to the folder you copied Shoelace's assets to -setBasePath('/shoelace'); +setBasePath("/shoelace"); import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js"; @@ -18,6 +18,8 @@ import { Session } from "../session"; import { VirtualMachine } from "../virtual_machine"; import { openFile, saveFile } from "../utils"; +import "../virtual_machine/ui"; + @customElement("ic10emu-app") export class App extends BaseElement { static styles = [ @@ -34,11 +36,7 @@ export class App extends BaseElement { } .app-body { flex-grow: 1; - // z-index: auto; } - // .z-fix { - // z-index: 900; - // } sl-split-panel { height: 100%; } @@ -47,8 +45,8 @@ export class App extends BaseElement { editorSettings: { fontSize: number; relativeLineNumbers: boolean }; - @query('ace-ic10') accessor editor: IC10Editor; - @query('session-share-dialog') accessor shareDialog: ShareSessionDialog; + @query("ace-ic10") accessor editor: IC10Editor; + @query("session-share-dialog") accessor shareDialog: ShareSessionDialog; // get editor() { // return this.renderRoot.querySelector("ace-ic10") as IC10Editor; @@ -62,14 +60,13 @@ export class App extends BaseElement { window.App = this; this.session = new Session(); this.vm = new VirtualMachine(); - } protected createRenderRoot(): HTMLElement | DocumentFragment { const root = super.createRenderRoot(); - root.addEventListener('app-share-session', this._handleShare.bind(this)); - root.addEventListener('app-open-file', this._handleOpenFile.bind(this)); - root.addEventListener('app-save-as', this._handleSaveAs.bind(this)); + root.addEventListener("app-share-session", this._handleShare.bind(this)); + root.addEventListener("app-open-file", this._handleOpenFile.bind(this)); + root.addEventListener("app-save-as", this._handleSaveAs.bind(this)); return root; } @@ -85,7 +82,7 @@ export class App extends BaseElement { snap-threshold="15" > -
Controls
+
@@ -93,8 +90,7 @@ export class App extends BaseElement { `; } - firstUpdated(): void { - } + firstUpdated(): void {} _handleShare(_e: Event) { // TODO: @@ -109,7 +105,6 @@ export class App extends BaseElement { _handleOpenFile(_e: Event) { openFile(window.Editor.editor); } - } declare global { diff --git a/www/src/js/components/base.ts b/www/src/js/components/base.ts index 37be34c..e6b8963 100644 --- a/www/src/js/components/base.ts +++ b/www/src/js/components/base.ts @@ -29,6 +29,14 @@ export const defaultCss = [ .mt-auto { margin-top: auto !important; } + .hstack { + display: flex; + flex-direction: row; + } + .vstack { + display: flex; + flex-direction: column; + } `, ]; diff --git a/www/src/js/editor/ace.ts b/www/src/js/editor/ace.ts index 7aa4549..6734181 100644 --- a/www/src/js/editor/ace.ts +++ b/www/src/js/editor/ace.ts @@ -20,6 +20,7 @@ export async function setupLspWorker() { return worker; } +export import Ace = ace.Ace; export import EditSession = ace.Ace.EditSession; export import Editor = ace.Ace.Editor; import { Range } from "ace-builds"; diff --git a/www/src/js/editor/index.ts b/www/src/js/editor/index.ts index 2d21c65..9052b9c 100644 --- a/www/src/js/editor/index.ts +++ b/www/src/js/editor/index.ts @@ -1,5 +1,6 @@ import { ace, + Ace, Editor, EditSession, Range, @@ -32,8 +33,8 @@ import { html } from "lit"; import { Ref, createRef, ref } from "lit/directives/ref.js"; import { customElement, property, query } from "lit/decorators.js"; import { editorStyles } from "./styles"; -import "./shortcuts-ui"; -import { AceKeyboardShortcuts } from "./shortcuts-ui"; +import "./shortcuts_ui"; +import { AceKeyboardShortcuts } from "./shortcuts_ui"; @customElement("ace-ic10") export class IC10Editor extends BaseElement { @@ -71,9 +72,9 @@ export class IC10Editor extends BaseElement { stylesAdded: string[]; tooltipObserver: MutationObserver; - @query('.e-kb-shortcuts') accessor kbShortcuts: AceKeyboardShortcuts; + @query(".e-kb-shortcuts") accessor kbShortcuts: AceKeyboardShortcuts; - @query('.e-settings-dialog') accessor settingDialog: SlDialog; + @query(".e-settings-dialog") accessor settingDialog: SlDialog; constructor() { super(); @@ -294,31 +295,30 @@ export class IC10Editor extends BaseElement { window.App!.session.loadFromFragment(); window.App!.session.onActiveLine(((e: CustomEvent) => { - const session = e.detail; - for (const id of session.programs.keys()) { - const active_line = session.getActiveLine(id); - if (typeof active_line !== "undefined") { - const marker = that.active_line_markers.get(id); - if (marker) { - that.sessions.get(id)?.removeMarker(marker); - that.active_line_markers.set(id, null); - } - const session = that.sessions.get(id); - if (session) { - that.active_line_markers.set( - id, - session.addMarker( - new Range(active_line, 0, active_line, 1), - "vm_ic_active_line", - "fullLine", - true, - ), - ); - if (that.active_session == id) { - // editor.resize(true); - // TODO: Scroll to line if vm was stepped - //that.editor.scrollToLine(active_line, true, true, ()=>{}) - } + const session = window.App?.session!; + const id = e.detail; + const active_line = session.getActiveLine(id); + if (typeof active_line !== "undefined") { + const marker = that.active_line_markers.get(id); + if (marker) { + that.sessions.get(id)?.removeMarker(marker); + that.active_line_markers.set(id, null); + } + const session = that.sessions.get(id); + if (session) { + that.active_line_markers.set( + id, + session.addMarker( + new Range(active_line, 0, active_line, 1), + "vm_ic_active_line", + "fullLine", + true, + ), + ); + if (that.active_session == id) { + // editor.resize(true); + // TODO: Scroll to line if vm was stepped + //that.editor.scrollToLine(active_line, true, true, ()=>{}) } } } diff --git a/www/src/js/editor/shortcuts-ui.ts b/www/src/js/editor/shortcuts_ui.ts similarity index 100% rename from www/src/js/editor/shortcuts-ui.ts rename to www/src/js/editor/shortcuts_ui.ts diff --git a/www/src/js/editor/styles.ts b/www/src/js/editor/styles.ts index 2e2bd36..746d3e6 100644 --- a/www/src/js/editor/styles.ts +++ b/www/src/js/editor/styles.ts @@ -316,6 +316,11 @@ export const editorStyles = css` background: rgba(76, 87, 103, 0.19); } + .vm_ic_active_line { + position: absolute; + background: rgba(121, 82, 179, 0.4); + z-index: 20; + } /* ---------------------- * Editor Setting dialog * ---------------------- */ diff --git a/www/src/js/session.ts b/www/src/js/session.ts index 296dfba..b34a445 100644 --- a/www/src/js/session.ts +++ b/www/src/js/session.ts @@ -60,17 +60,21 @@ j ra `; +import type { ICError } from "ic10emu_wasm"; + export class Session extends EventTarget { _programs: Map; - _activeSession: number; + _errors: Map; + _activeIC: number; _activeLines: Map; _activeLine: number; _save_timeout?: ReturnType; constructor() { super(); this._programs = new Map(); + this._errors = new Map(); this._save_timeout = undefined; - this._activeSession = 0; + this._activeIC = 0; this._activeLines = new Map(); this.loadFromFragment(); @@ -86,10 +90,26 @@ export class Session extends EventTarget { set programs(programs) { this._programs = new Map([...programs]); + this._fireOnLoad(); } - get activeSession() { - return this._activeSession; + get activeIC() { + return this._activeIC; + } + + set activeIC(val: number) { + this._activeIC = val; + this.dispatchEvent( + new CustomEvent("session-active-ic", { detail: this.activeIC }), + ); + } + + onActiveIc(callback: EventListenerOrEventListenerObject) { + this.addEventListener("session-active-ic", callback); + } + + get errors() { + return this._errors; } getActiveLine(id: number) { @@ -98,7 +118,7 @@ export class Session extends EventTarget { setActiveLine(id: number, line: number) { this._activeLines.set(id, line); - this._fireOnActiveLine(); + this._fireOnActiveLine(id); } set activeLine(line: number) { @@ -110,6 +130,23 @@ export class Session extends EventTarget { this.save(); } + setProgramErrors(id: number, errors: ICError[]) { + this._errors.set(id, errors); + this._fireOnErrors([id]); + } + + _fireOnErrors(ids: number[]) { + this.dispatchEvent( + new CustomEvent("session-errors", { + detail: ids, + }), + ); + } + + onErrors(callback: EventListenerOrEventListenerObject) { + this.addEventListener("session-errors", callback); + } + onLoad(callback: EventListenerOrEventListenerObject) { this.addEventListener("session-load", callback); } @@ -126,10 +163,10 @@ export class Session extends EventTarget { this.addEventListener("active-line", callback); } - _fireOnActiveLine() { + _fireOnActiveLine(id: number) { this.dispatchEvent( - new CustomEvent("activeLine", { - detail: this, + new CustomEvent("active-line", { + detail: id, }), ); } @@ -269,4 +306,3 @@ async function decompress(bytes: ArrayBuffer) { } return await concatUintArrays(chunks); } - diff --git a/www/src/js/utils.ts b/www/src/js/utils.ts index 68b3bdb..8beb357 100644 --- a/www/src/js/utils.ts +++ b/www/src/js/utils.ts @@ -1,6 +1,6 @@ import { Ace } from "ace-builds"; -function docReady(fn: () => void) { +export function docReady(fn: () => void) { // see if DOM is already available if ( document.readyState === "complete" || @@ -12,8 +12,44 @@ function docReady(fn: () => void) { } } + +function replacer(key: any, value: any) { + if(value instanceof Map) { + return { + dataType: 'Map', + value: Array.from(value.entries()), // or with spread: value: [...value] + }; + } else { + return value; + } +} + +function reviver(_key: any, value: any) { + if(typeof value === 'object' && value !== null) { + if (value.dataType === 'Map') { + return new Map(value.value); + } + } + return value; +} + +export function toJson(value: any): string { + return JSON.stringify(value, replacer); +} + +export function fromJson(value: string): any { + return JSON.parse(value, reviver) +} + +export function structuralEqual(a: any, b: any): boolean { + const _a = JSON.stringify(a, replacer); + const _b = JSON.stringify(b, replacer); + return _a === _b; + +} + // probably not needed, fetch() exists now -function makeRequest(opts: { +export function makeRequest(opts: { method: string; url: string; headers: { [key: string]: string }; @@ -57,7 +93,7 @@ function makeRequest(opts: { }); } -async function saveFile(content: BlobPart) { +export async function saveFile(content: BlobPart) { const blob = new Blob([content], { type: "text/plain" }); if (typeof window.showSaveFilePicker !== "undefined") { console.log("Saving via FileSystem API"); @@ -88,7 +124,7 @@ async function saveFile(content: BlobPart) { } } -async function openFile(editor: Ace.Editor) { +export async function openFile(editor: Ace.Editor) { if (typeof window.showOpenFilePicker !== "undefined") { console.log("opening file via FileSystem Api"); try { @@ -121,4 +157,3 @@ async function openFile(editor: Ace.Editor) { input.click(); } } -export { docReady, makeRequest, saveFile, openFile }; diff --git a/www/src/js/virtual_machine/base_device.ts b/www/src/js/virtual_machine/base_device.ts new file mode 100644 index 0000000..9536045 --- /dev/null +++ b/www/src/js/virtual_machine/base_device.ts @@ -0,0 +1,78 @@ +import { property, state } from "lit/decorators.js"; +import { BaseElement } from "../components"; +import { + DeviceRef, + Fields, + Reagents, + Slot, + Connection, +} from "ic10emu_wasm"; +import { structuralEqual } from "../utils"; + +export class VMBaseDevice extends BaseElement { + @property({ type: Number }) accessor deviceID: number; + @state() protected accessor device: DeviceRef; + + @state() accessor name: string | null; + @state() accessor nameHash: number | null; + @state() accessor prefabName: string | null; + @state() accessor fields: Fields; + @state() accessor slots: Slot[]; + @state() accessor reagents: Reagents; + @state() accessor connections: Connection[]; + + constructor() { + super(); + this.name = null; + this.nameHash = null; + } + + connectedCallback(): void { + const root = super.connectedCallback(); + this.device = window.VM!.devices.get(this.deviceID)!; + window.VM?.addEventListener( + "vm-device-modified", + this._handleDeviceModified.bind(this), + ); + this.updateDevice(); + return root; + } + + _handleDeviceModified(e: CustomEvent) { + const id = e.detail; + if (this.deviceID === id) { + this.updateDevice(); + } + } + + updateDevice() { + const name = this.device.name ?? null; + if (this.name !== name) { + this.name = name; + } + const nameHash = this.device.nameHash ?? null; + if (this.nameHash !== nameHash) { + this.nameHash = nameHash; + } + const prefabName = this.device.prefabName ?? null; + if (this.prefabName !== prefabName) { + this.prefabName = prefabName; + } + const fields = this.device.fields; + if (!structuralEqual(this.fields, fields)) { + this.fields = fields; + } + const slots = this.device.slots; + if (!structuralEqual(this.slots, slots)) { + this.slots = slots; + } + const reagents = this.device.reagents; + if (!structuralEqual(this.reagents, reagents)) { + this.reagents = reagents; + } + const connections = this.device.connections; + if (!structuralEqual(this.connections, connections)) { + this.connections = connections; + } + } +} diff --git a/www/src/js/virtual_machine/controls.ts b/www/src/js/virtual_machine/controls.ts new file mode 100644 index 0000000..e345095 --- /dev/null +++ b/www/src/js/virtual_machine/controls.ts @@ -0,0 +1,190 @@ +import { html, css } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { defaultCss } from "../components"; +import { DeviceRef, ICError } from "ic10emu_wasm"; +import { VMBaseDevice } from "./base_device"; +import { structuralEqual } from "../utils"; + +import "@shoelace-style/shoelace/dist/components/card/card.js"; +import "@shoelace-style/shoelace/dist/components/button-group/button-group.js"; +import "@shoelace-style/shoelace/dist/components/button/button.js"; +import "@shoelace-style/shoelace/dist/components/icon/icon.js"; +import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js"; +import "@shoelace-style/shoelace/dist/components/divider/divider.js"; + +@customElement("vm-ic-controls") +export class VMICControls extends VMBaseDevice { + @state() accessor icIP: number; + @state() accessor icOpCount: number; + @state() accessor icState: string; + @state() accessor errors: ICError[]; + + static styles = [ + ...defaultCss, + css` + :host { + } + .card { + margin-left: 1rem; + margin-right: 1rem; + margin-top: 1rem; + } + .controls { + display: flex; + flex-direction: row; + font-size: var(--sl-font-size-small); + } + .stats { + font-size: var(--sl-font-size-x-small); + } + .device-id { + margin-left: 2rem; + } + .button-group-toolbar sl-button-group:not(:last-of-type) { + margin-right: var(--sl-spacing-x-small); + } + sl-divider { + --spacing: 0.25rem; + } + `, + ]; + + constructor() { + super(); + this.deviceID = window.App!.session.activeIC; + } + + connectedCallback(): void { + const root = super.connectedCallback(); + window.VM?.addEventListener( + "vm-run-ic", + this._handleDeviceModified.bind(this), + ); + window.App?.session.addEventListener( + "session-active-ic", + this._handleActiveIC.bind(this), + ); + this.updateIC(); + return root; + } + + _handleActiveIC(e: CustomEvent) { + const id = e.detail; + if (this.deviceID !== id) { + this.deviceID = id; + this.device = window.VM!.devices.get(this.deviceID)!; + } + this.updateDevice(); + } + + updateIC() { + const ip = this.device.ip!; + if (this.icIP !== ip) { + this.icIP = ip; + } + const opCount = this.device.instructionCount!; + if (this.icOpCount !== opCount) { + this.icOpCount = opCount; + } + const state = this.device.state!; + if (this.icState !== state) { + this.icState = state; + } + const errors = this.device.program!.errors; + if (!structuralEqual(this.errors, errors)) { + this.errors = errors; + } + } + + updateDevice(): void { + super.updateDevice(); + this.updateIC(); + } + + protected firstUpdated(): void {} + + protected render() { + return html` + +
+ + + + Run + + + + + + Step + + + + + + Reset + + + + +
+ Device: + ${this.deviceID}${this.name ?? this.prefabName + ? ` : ${this.name ?? this.prefabName}` + : ""} +
+
+
+
+ Instruction Pointer + ${this.icIP} +
+ +
+ Last Run Operations Count + ${this.icOpCount} +
+ +
+ Last State + ${this.icState} +
+ +
+ Errors + ${this.errors.map( + (err) => + html`
+ + Line: ${err.ParseError.line} - + ${err.ParseError.start}:${err.ParseError.end} + + ${err.ParseError.msg} +
`, + )} +
+
+
+ `; + } + + _handleRunClick() { + window.VM?.run(); + } + _handleStepClick() { + window.VM?.step() + } + _handleResetClick() { + window.VM?.reset() + } +} diff --git a/www/src/js/virtual_machine/index.ts b/www/src/js/virtual_machine/index.ts index b1b3a1e..79b4af1 100644 --- a/www/src/js/virtual_machine/index.ts +++ b/www/src/js/virtual_machine/index.ts @@ -1,7 +1,5 @@ import { DeviceRef, VM, init } from "ic10emu_wasm"; -import { VMDeviceUI } from "./device"; -import { BaseElement } from "../components"; -// import { Card } from 'bootstrap'; +import "./base_device"; declare global { interface Window { @@ -26,7 +24,7 @@ type DeviceDB = { }; }; -class VirtualMachine { +class VirtualMachine extends EventTarget { ic10vm: VM; ui: VirtualMachineUI; _devices: Map; @@ -34,12 +32,12 @@ class VirtualMachine { db: DeviceDB; constructor() { + super(); const vm = init(); window.VM = this; this.ic10vm = vm; - // this.ui = new VirtualMachineUI(this); this._devices = new Map(); this._ics = new Map(); @@ -58,7 +56,7 @@ class VirtualMachine { } get activeIC() { - return this._ics.get(window.App!.session.activeSession); + return this._ics.get(window.App!.session.activeIC); } updateDevices() { @@ -98,7 +96,12 @@ class VirtualMachine { if (ic && prog) { console.time(`CompileProgram_${id}_${attempt}`); try { - this.ics.get(id)!.setCode(progs.get(id)!); + this.ics.get(id)!.setCodeInvalid(progs.get(id)!); + const compiled = this.ics.get(id)?.program!; + window.App?.session.setProgramErrors(id, compiled.errors); + this.dispatchEvent( + new CustomEvent("vm-device-modified", { detail: id }), + ); } catch (e) { console.log(e); } @@ -117,6 +120,9 @@ class VirtualMachine { console.log(e); } this.update(); + this.dispatchEvent( + new CustomEvent("vm-run-ic", { detail: this.activeIC!.id }), + ); } } @@ -129,6 +135,9 @@ class VirtualMachine { console.log(e); } this.update(); + this.dispatchEvent( + new CustomEvent("vm-run-ic", { detail: this.activeIC!.id }), + ); } } @@ -142,24 +151,36 @@ class VirtualMachine { update() { this.updateDevices(); + this.ic10vm.lastOperationModified.forEach((id, _index, _modifiedIds) => { + if (this.devices.has(id)) { + this.dispatchEvent( + new CustomEvent("vm-device-modified", { detail: id }), + ); + } + }, this); const ic = this.activeIC!; - window.App!.session.setActiveLine(window.App!.session.activeSession, ic.ip!); - // this.ui.update(ic); + window.App!.session.setActiveLine(window.App!.session.activeIC, ic.ip!); } setRegister(index: number, val: number) { const ic = this.activeIC!; try { ic.setRegister(index, val); + this.dispatchEvent( + new CustomEvent("vm-device-modified", { detail: ic.id }), + ); } catch (e) { console.log(e); } } setStack(addr: number, val: number) { - const ic = this.activeIC; + const ic = this.activeIC!; try { ic!.setStack(addr, val); + this.dispatchEvent( + new CustomEvent("vm-device-modified", { detail: ic.id }), + ); } catch (e) { console.log(e); } @@ -176,14 +197,12 @@ class VirtualMachineUI { state: VMStateUI; registers: VMRegistersUI; stack: VMStackUI; - devices: VMDeviceUI; constructor(vm: VirtualMachine) { this.vm = vm; this.state = new VMStateUI(this); this.registers = new VMRegistersUI(this); this.stack = new VMStackUI(this); - this.devices = new VMDeviceUI(this); const that = this; @@ -214,7 +233,6 @@ class VirtualMachineUI { this.state.update(ic); this.registers.update(ic); this.stack.update(ic); - this.devices.update(ic); } } diff --git a/www/src/js/virtual_machine/ui.ts b/www/src/js/virtual_machine/ui.ts new file mode 100644 index 0000000..31cf7f7 --- /dev/null +++ b/www/src/js/virtual_machine/ui.ts @@ -0,0 +1,17 @@ +import { HTMLTemplateResult, html, css } from "lit"; +import { customElement, property, query } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "../components"; + +import "./controls.ts"; + +@customElement("vm-ui") +export class VMUI extends BaseElement { + + constructor() { + super(); + } + + protected render() { + return html``; + } +}