From 1840bf657519c0eb73331dfd6bc0f9068aac0485 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Tue, 23 Jul 2019 11:03:35 +0200 Subject: [PATCH] Added assembler worker code. --- src/scripting/assembly.test.ts | 6 +- src/scripting/assembly.ts | 66 +++++----- src/scripting/assembly_worker_init.ts | 120 ++++++++++++++++++ src/ui/quest_editor/ScriptEditorComponent.tsx | 14 +- tsconfig.json | 46 +++---- 5 files changed, 190 insertions(+), 62 deletions(-) create mode 100644 src/scripting/assembly_worker_init.ts diff --git a/src/scripting/assembly.test.ts b/src/scripting/assembly.test.ts index 73a19a98..b23fee3e 100644 --- a/src/scripting/assembly.test.ts +++ b/src/scripting/assembly.test.ts @@ -2,7 +2,8 @@ import { assemble } from "./assembly"; import { Opcode } from "../data_formats/parsing/quest/bin"; test("", () => { - const { instructions, labels, errors } = assemble(` + const { instructions, labels, errors } = assemble( + ` 0: set_episode 0 bb_map_designate 1, 2, 3, 4 set_floor_handler 0, 150 @@ -11,7 +12,8 @@ test("", () => { 150: set_mainwarp 1 ret 151: ret - `); + `.split("\n") + ); expect(errors).toEqual([]); diff --git a/src/scripting/assembly.ts b/src/scripting/assembly.ts index 8e2e82fe..5779e9b5 100644 --- a/src/scripting/assembly.ts +++ b/src/scripting/assembly.ts @@ -7,15 +7,15 @@ import { Param, } from "../data_formats/parsing/quest/bin"; -type AssemblyError = { - line: number; +export type AssemblyError = { + line_no: number; col: number; length: number; message: string; }; export function assemble( - assembly: string, + assembly: string[], manual_stack: boolean = false ): { instructions: Instruction[]; @@ -26,21 +26,21 @@ export function assemble( const instructions: Instruction[] = []; const labels = new Map(); - let line = 1; + let line_no = 1; - for (const line_text of assembly.split("\n")) { - const match = line_text.match( + for (const line of assembly) { + const match = line.match( /^(?\s*)(?[^\s]+?:)?(?\s*)(?[a-z][a-z0-9_=<>!]*)?(?.*)$/ ); if (!match || !match.groups || (match.groups.lbl == null && match.groups.op == null)) { - const left_trimmed = line_text.trimLeft(); + const left_trimmed = line.trimLeft(); const trimmed = left_trimmed.trimRight(); if (trimmed.length) { errors.push({ - line, - col: 1 + line_text.length - left_trimmed.length, + line_no, + col: 1 + line.length - left_trimmed.length, length: trimmed.length, message: "Expected label or instruction.", }); @@ -53,14 +53,14 @@ export function assemble( if (!isFinite(label) || !/^\d+:$/.test(lbl)) { errors.push({ - line, + line_no, col: 1 + lbl_ws.length, length: lbl.length, message: "Invalid label name.", }); } else if (labels.has(label)) { errors.push({ - line, + line_no, col: 1 + lbl_ws.length, length: lbl.length - 1, message: "Duplicate label.", @@ -75,7 +75,7 @@ export function assemble( if (!opcode) { errors.push({ - line, + line_no, col: 1 + lbl_ws.length + (lbl ? lbl.length : 0) + op_ws.length, length: op.length, message: "Unknown instruction.", @@ -98,7 +98,7 @@ export function assemble( const trimmed = args.trimRight(); errors.push({ - line, + line_no, col: args_col + args.length - left_trimmed.length, length: trimmed.length, message: "Instruction arguments expected.", @@ -117,10 +117,10 @@ export function assemble( ? arg_tokens.length < param_count : arg_tokens.length !== param_count ) { - const left_trimmed = line_text.trimLeft(); + const left_trimmed = line.trimLeft(); errors.push({ - line, - col: 1 + line_text.length - left_trimmed.length, + line_no, + col: 1 + line.length - left_trimmed.length, length: left_trimmed.length, message: `Expected${ varargs ? " at least" : "" @@ -129,10 +129,16 @@ export function assemble( }.`, }); } else if (varargs || arg_tokens.length === opcode.params.length) { - parse_args(opcode.params, arg_tokens, ins_args, line, errors); + parse_args(opcode.params, arg_tokens, ins_args, line_no, errors); } else { const stack_args: Arg[] = []; - parse_args(opcode.stack_params, arg_tokens, stack_args, line, errors); + parse_args( + opcode.stack_params, + arg_tokens, + stack_args, + line_no, + errors + ); for (let i = 0; i < opcode.stack_params.length; i++) { const param = opcode.stack_params[i]; @@ -162,7 +168,7 @@ export function assemble( break; default: errors.push({ - line, + line_no, col, length, message: `Type ${Type[param.type]} not implemented.`, @@ -177,7 +183,7 @@ export function assemble( } } - line++; + line_no++; } return { @@ -262,7 +268,7 @@ function parse_args( return; default: errors.push({ - line, + line_no: line, col, length, message: `Type ${Type[param.type]} not implemented.`, @@ -285,14 +291,14 @@ function parse_uint( if (!/^\d+$/.test(arg_str)) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `Expected unsigned integer.`, }); } else if (value > max_value) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `${bit_size}-Bit unsigned integer can't be greater than ${max_value}.`, @@ -320,21 +326,21 @@ function parse_sint( if (!/^-?\d+$/.test(arg_str)) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `Expected signed integer.`, }); } else if (value < min_value) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `${bit_size}-Bit signed integer can't be less than ${min_value}.`, }); } else if (value > max_value) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `${bit_size}-Bit signed integer can't be greater than ${max_value}.`, @@ -358,7 +364,7 @@ function parse_float( if (!Number.isFinite(value)) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `Expected floating point number.`, @@ -382,14 +388,14 @@ function parse_register( if (!/^r\d+$/.test(arg_str)) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `Expected register reference.`, }); } else if (value > 255) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `Invalid register reference, expected r0-r255.`, @@ -411,7 +417,7 @@ function parse_string( ): void { if (!/^"([^"\\]|\\.)*"$/.test(arg_str)) { errors.push({ - line, + line_no: line, col, length: arg_str.length, message: `Expected string.`, diff --git a/src/scripting/assembly_worker_init.ts b/src/scripting/assembly_worker_init.ts new file mode 100644 index 00000000..5cad107d --- /dev/null +++ b/src/scripting/assembly_worker_init.ts @@ -0,0 +1,120 @@ +import { editor } from "monaco-editor"; +import { assemble, AssemblyError } from "./assembly"; + +interface ScriptWorkerInput {} + +class NewModelInput implements ScriptWorkerInput { + constructor(readonly value: string) {} +} + +class ModelChangeInput implements ScriptWorkerInput { + constructor(readonly changes: editor.IModelContentChange[]) {} +} + +interface ScriptWorkerOutput {} + +class NewErrorsOutput implements ScriptWorkerOutput { + constructor(readonly errors: AssemblyError[]) {} +} + +let lines: string[] = []; + +function replace_line_part( + line_no: number, + start_col: number, + end_col: number, + new_line_parts: string[] +): void { + const line = lines[line_no - 1]; + // We keep the parts of the line that weren't affected by the edit. + const line_start = line.slice(0, start_col - 1); + const line_end = line.slice(end_col); + + if (new_line_parts.length === 1) { + lines.splice(line_no - 1, 1, line_start + new_line_parts[0] + line_end); + } else { + lines.splice( + line_no - 1, + 1, + line_start + new_line_parts[0], + ...new_line_parts.slice(1, new_line_parts.length - 2), + new_line_parts[new_line_parts.length - 1] + line_end + ); + } +} + +function replace_line_part_left(line_no: number, end_col: number, new_line_part: string): void { + lines.splice(line_no - 1, 1, new_line_part + lines[line_no - 1].slice(end_col)); +} + +function replace_line_part_right(line_no: number, start_col: number, new_line_part: string): void { + lines.splice(line_no - 1, 1, lines[line_no - 1].slice(0, start_col - 1) + new_line_part); +} + +function replace_lines(start_line_no: number, end_line_no: number, new_lines: string[]): void { + lines.splice(start_line_no - 1, end_line_no - start_line_no + 1, ...new_lines); +} + +function replace_lines_and_merge_line_parts( + start_line_no: number, + end_line_no: number, + start_col: number, + end_col: number, + new_line_part: string +): void { + const start_line = lines[start_line_no - 1]; + const end_line = lines[end_line_no - 1]; + // We keep the parts of the lines that weren't affected by the edit. + const start_line_start = start_line.slice(0, start_col - 1); + const end_line_end = end_line.slice(end_col); + + lines.splice( + start_line_no - 1, + end_line_no - start_line_no + 1, + start_line_start + new_line_part + end_line_end + ); +} + +window.onmessage = (e: MessageEvent) => { + const message: ScriptWorkerInput = e.data; + + if (message instanceof NewModelInput) { + lines = message.value.split("\n"); + window.postMessage(assemble(lines).errors, window.origin); + } else if (message instanceof ModelChangeInput) { + for (const change of message.changes) { + const { startLineNumber, endLineNumber, startColumn, endColumn } = change.range; + const lines_changed = endLineNumber - startLineNumber + 1; + const new_lines = change.text.split("\n"); + + if (lines_changed === 1) { + replace_line_part(startLineNumber, startColumn, endColumn, new_lines); + } else if (new_lines.length === 1) { + replace_lines_and_merge_line_parts( + startLineNumber, + endLineNumber, + startColumn, + endColumn, + new_lines[0] + ); + } else { + // Keep the left part of the first changed line. + replace_line_part_right(startLineNumber, startColumn, new_lines[0]); + + // Replace all the lines in between. + replace_lines( + startLineNumber + 1, + endLineNumber - 1, + new_lines.slice(1, new_lines.length - 2) + ); + + // Keep the right part of the last changed line. + replace_line_part_left(startLineNumber, endColumn, new_lines[new_lines.length - 1]); + } + } + + window.postMessage(assemble(lines).errors, window.origin); + } else { + throw new Error("Couldn't process message."); + } +}; diff --git a/src/ui/quest_editor/ScriptEditorComponent.tsx b/src/ui/quest_editor/ScriptEditorComponent.tsx index 1e314722..646b0427 100644 --- a/src/ui/quest_editor/ScriptEditorComponent.tsx +++ b/src/ui/quest_editor/ScriptEditorComponent.tsx @@ -195,13 +195,19 @@ class MonacoComponent extends Component { } } - private validate = () => { + private validate = (e?: editor.IModelContentChangedEvent) => { if (!this.editor) return; const model = this.editor.getModel(); if (!model) return; - const { instructions, labels, errors } = assemble(model.getValue()); + if (e) { + e.changes.forEach(change => { + console.log(change); + }); + } + + const { instructions, labels, errors } = assemble(model.getLinesContent()); if (quest_editor_store.current_quest) { quest_editor_store.current_quest.instructions = instructions; @@ -214,8 +220,8 @@ class MonacoComponent extends Component { errors.map(error => ({ severity: MarkerSeverity.Error, message: error.message, - startLineNumber: error.line, - endLineNumber: error.line, + startLineNumber: error.line_no, + endLineNumber: error.line_no, startColumn: error.col, endColumn: error.col + error.length, })) diff --git a/tsconfig.json b/tsconfig.json index b3583351..f0cf16e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,21 @@ { - "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve", - "experimentalDecorators": true, - "downlevelIteration": true - }, - "include": [ - "src" - ] -} \ No newline at end of file + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "experimentalDecorators": true, + "downlevelIteration": true + }, + "include": ["src"] +}